|
|
@@ -4,123 +4,164 @@
|
|
|
<view class="create-head">
|
|
|
<view class="head-avatar">{{ avatar }}</view>
|
|
|
<view class="head-info">
|
|
|
- <text class="head-name">{{ form.name || '新员工姓名' }}</text>
|
|
|
+ <text class="head-name">{{ form.name || (form.createMode === 'invite' ? '邀请候选人' : '新员工姓名') }}</text>
|
|
|
<text class="head-sub">{{ subPreview }}</text>
|
|
|
</view>
|
|
|
<text class="status-tag status-pending">待入职</text>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 入职流程提示 -->
|
|
|
- <view class="steps">
|
|
|
- <view class="step active"><view>1</view><text>录入信息</text></view>
|
|
|
- <view class="step-line"></view>
|
|
|
- <view class="step"><view>2</view><text>材料收集</text></view>
|
|
|
- <view class="step-line"></view>
|
|
|
- <view class="step"><view>3</view><text>签订合同</text></view>
|
|
|
- <view class="step-line"></view>
|
|
|
- <view class="step"><view>4</view><text>到岗入职</text></view>
|
|
|
+ <!-- 办理方式 -->
|
|
|
+ <view class="section-title"><text>办理方式</text></view>
|
|
|
+ <view class="mode-row section">
|
|
|
+ <view class="radio-card" :class="{ 'is-checked': form.createMode === 'manual' }"
|
|
|
+ @click="form.createMode = 'manual'">
|
|
|
+ <text class="mode-title">手动添加</text>
|
|
|
+ <text class="mode-desc">HR 直接录入员工档案</text>
|
|
|
+ </view>
|
|
|
+ <view class="radio-card" :class="{ 'is-checked': form.createMode === 'invite' }"
|
|
|
+ @click="form.createMode = 'invite'">
|
|
|
+ <text class="mode-title">二维码邀请</text>
|
|
|
+ <text class="mode-desc">生成链接由候选人填写</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="form.createMode === 'invite'" class="section">
|
|
|
+ <view class="form-card card section">
|
|
|
+ <view class="field press" @click="chooseValidDays"><text class="label">链接有效期</text><text class="value">{{
|
|
|
+ form.validDays }} 天</text><text class="arrow">›</text></view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 基础信息 -->
|
|
|
<view class="section-title"><text>基础信息</text></view>
|
|
|
<view class="form-card card section">
|
|
|
- <view class="field"
|
|
|
- ><text class="label required">姓名</text>
|
|
|
+ <view class="field"><text class="label required">姓名</text>
|
|
|
<input v-model="form.name" class="value-input" placeholder="请输入候选人姓名" maxlength="20" />
|
|
|
</view>
|
|
|
<view class="divider"></view>
|
|
|
- <view class="field press" @click="chooseDept"
|
|
|
- ><text class="label required">部门</text
|
|
|
- ><text :class="form.department ? 'value' : 'placeholder'">{{ form.department || '请选择' }}</text
|
|
|
- ><text class="arrow">›</text
|
|
|
- ></view>
|
|
|
+ <view class="field press"><text class="label required">入职日期</text>
|
|
|
+ <picker mode="date" :value="form.hireDate" :start="todayIso" @change="onHireDateChange"><text
|
|
|
+ :class="form.hireDate ? 'value' : 'placeholder'">{{
|
|
|
+ form.hireDate || '请选择'
|
|
|
+ }}</text></picker><text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
<view class="divider"></view>
|
|
|
- <view class="field"
|
|
|
- ><text class="label required">岗位</text>
|
|
|
- <input v-model="form.position" class="value-input" placeholder="请输入岗位名称" maxlength="30" />
|
|
|
+ <view class="field press" @click="chooseLegal"><text class="label required">所属法人</text><text
|
|
|
+ :class="form.legalName ? 'value' : 'placeholder'">{{
|
|
|
+ form.legalName || '请选择'
|
|
|
+ }}</text><text class="arrow">›</text></view>
|
|
|
+ <view class="divider"></view>
|
|
|
+ <view class="field press" @click="onDeptFieldTap"><text class="label required">目标部门</text>
|
|
|
+ <picker v-if="form.legalId" mode="multiSelector" :range="deptColumns" :value="deptIndex" range-key="name"
|
|
|
+ @columnchange="onDeptColChange" @change="onDeptPick">
|
|
|
+ <text :class="form.deptPath ? 'value' : 'placeholder'">{{
|
|
|
+ form.deptPath || '请选择'
|
|
|
+ }}</text>
|
|
|
+ </picker>
|
|
|
+ <text v-else class="placeholder">请先选择法人</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
+ <view class="divider"></view>
|
|
|
+ <view class="field press" @click="choosePosition"><text class="label required">岗位</text>
|
|
|
+ <text :class="form.positionName ? 'value' : 'placeholder'">{{
|
|
|
+ form.positionName || '请选择'
|
|
|
+ }}</text><text class="arrow">›</text>
|
|
|
</view>
|
|
|
<view class="divider"></view>
|
|
|
- <view class="field press" @click="chooseSource"
|
|
|
- ><text class="label required">招聘来源</text
|
|
|
- ><text :class="form.source ? 'value' : 'placeholder'">{{ form.source || '请选择' }}</text
|
|
|
- ><text class="arrow">›</text
|
|
|
- ></view>
|
|
|
+ <view class="field press" @click="chooseEmployment"><text class="label">用工类型</text><text class="value">{{
|
|
|
+ employmentLabel }}</text><text class="arrow">›</text></view>
|
|
|
+ <view class="divider"></view>
|
|
|
+ <view class="field">
|
|
|
+ <text class="label">是否试用</text>
|
|
|
+ <view class="inline-radios">
|
|
|
+ <view class="radio-card" :class="{ 'is-checked': form.probationFlag === 1 }" @click="form.probationFlag = 1">
|
|
|
+ <text>是</text>
|
|
|
+ </view>
|
|
|
+ <view class="radio-card" :class="{ 'is-checked': form.probationFlag === 0 }" @click="form.probationFlag = 0">
|
|
|
+ <text>否</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view v-if="form.probationFlag === 1" class="divider"></view>
|
|
|
+ <view v-if="form.probationFlag === 1" class="field press" @click="chooseProbationMonths"><text
|
|
|
+ class="label">试用月数</text><text class="value">{{ form.probationMonths }} 个月</text><text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 联系方式 -->
|
|
|
<view class="section-title"><text>联系方式</text></view>
|
|
|
<view class="form-card card section">
|
|
|
- <view class="field"
|
|
|
- ><text class="label required">手机号</text>
|
|
|
- <input
|
|
|
- v-model="form.phone"
|
|
|
- class="value-input"
|
|
|
- type="number"
|
|
|
- placeholder="11位手机号"
|
|
|
- maxlength="11"
|
|
|
- />
|
|
|
+ <view class="field"><text class="label">手机号</text>
|
|
|
+ <input v-model="form.phone" class="value-input" type="number" placeholder="11位手机号" maxlength="11" />
|
|
|
</view>
|
|
|
<view class="divider"></view>
|
|
|
- <view class="field"
|
|
|
- ><text class="label required">邮箱</text>
|
|
|
+ <view class="field"><text class="label">邮箱</text>
|
|
|
<input v-model="form.email" class="value-input" placeholder="公司邮箱或外部邮箱" />
|
|
|
</view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 入职安排 -->
|
|
|
- <view class="section-title"><text>入职安排</text></view>
|
|
|
- <view class="form-card card section">
|
|
|
- <view class="field"
|
|
|
- ><text class="label required">预计入职日期</text>
|
|
|
- <picker mode="date" :value="form.expectedEntryDate" :start="todayIso" @change="onDateChange"
|
|
|
- ><text :class="form.expectedEntryDate ? 'value' : 'placeholder'">{{
|
|
|
- form.expectedEntryDate || '请选择'
|
|
|
- }}</text></picker
|
|
|
- ><text class="arrow">›</text>
|
|
|
- </view>
|
|
|
- <view class="divider"></view>
|
|
|
- <view class="field press" @click="chooseProbation"
|
|
|
- ><text class="label">试用期</text
|
|
|
- ><text class="value">{{ form.probationMonths }} 个月</text
|
|
|
- ><text class="arrow">›</text
|
|
|
- ></view>
|
|
|
<view class="divider"></view>
|
|
|
- <view class="field"
|
|
|
- ><text class="label">带教人</text>
|
|
|
- <input v-model="form.mentor" class="value-input" placeholder="可选" maxlength="20" />
|
|
|
+ <view class="field"><text class="label">身份证号</text>
|
|
|
+ <input v-model="form.idNumber" class="value-input" placeholder="可选" maxlength="18" />
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 备注 -->
|
|
|
<view class="section-title"><text>备注</text></view>
|
|
|
<view class="form-card card section">
|
|
|
- <textarea
|
|
|
- v-model="form.remark"
|
|
|
- class="textarea"
|
|
|
- placeholder="例如:提前到岗日期、特殊情况说明等"
|
|
|
- maxlength="200"
|
|
|
- />
|
|
|
+ <textarea v-model="form.remark" class="textarea" placeholder="例如:提前到岗日期、特殊情况说明等" maxlength="200" />
|
|
|
<text class="textarea-count">{{ form.remark.length }} / 200</text>
|
|
|
</view>
|
|
|
|
|
|
+ <!-- 邀请结果卡片 -->
|
|
|
+ <view v-if="showInviteCard && inviteResult" class="section">
|
|
|
+ <view class="invite-card card">
|
|
|
+ <view class="invite-head">
|
|
|
+ <text>邀请链接已生成</text>
|
|
|
+ <text class="status-tag status-approved">已生成</text>
|
|
|
+ </view>
|
|
|
+ <text class="muted">请将以下链接发送给候选人,{{ form.validDays }} 天内有效。</text>
|
|
|
+ <view class="invite-url">{{ inviteResult.publicPath }}</view>
|
|
|
+ <view class="invite-actions">
|
|
|
+ <view class="ghost-btn" @click="onCancel">返回列表</view>
|
|
|
+ <view class="primary-btn" @click="copyInviteLink">复制链接</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
<view class="footer-tip muted">
|
|
|
- <text>提交后将生成员工编号,并自动出现在「员工入职」列表中。</text>
|
|
|
+ <text v-if="form.createMode === 'manual'">提交后将生成员工编号,并自动出现在「员工入职」列表中。</text>
|
|
|
+ <text v-else>生成链接后,候选人填写提交的资料将自动归档到该入职流程。</text>
|
|
|
</view>
|
|
|
|
|
|
<view class="footer-actions">
|
|
|
<view class="ghost-btn" @click="onCancel">取消</view>
|
|
|
- <view class="primary-btn" @click="onSubmit">提交入职流程</view>
|
|
|
+ <view class="primary-btn" @click="onSubmit">
|
|
|
+ {{ form.createMode === 'manual' ? '提交入职流程' : '生成邀请链接' }}
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { computed, reactive } from "vue";
|
|
|
-import { onLoad } from "@dcloudio/uni-app";
|
|
|
-import { useAuthGuard } from "@/hooks/useAuthGuard";
|
|
|
-import { onboardOptions, generateEmployeeId } from "@/data/mock";
|
|
|
-import { addOnboarding, mergeOnboardings } from "@/utils/storage";
|
|
|
-import { go } from "@/utils/router";
|
|
|
+import { computed, reactive, ref } from 'vue';
|
|
|
+import { onLoad, onShow, onUnload } from '@dcloudio/uni-app';
|
|
|
+import { useAuthGuard } from '@/hooks/useAuthGuard';
|
|
|
+import { go } from '@/utils/router';
|
|
|
+import {
|
|
|
+ addOnboarding,
|
|
|
+ addOnboardingInvitation,
|
|
|
+ mergeOnboardings,
|
|
|
+ clearOnboardingDraft,
|
|
|
+} from '@/utils/storage';
|
|
|
+import { createOnboardingManual, createOnboardingInvitation } from '@/api/onboarding';
|
|
|
+import { listOrganizations } from '@/api/organization';
|
|
|
+import { pagePosition } from '@/api/position';
|
|
|
+import {
|
|
|
+ employmentTypes,
|
|
|
+ probationMonthOptions,
|
|
|
+ invitationValidDays,
|
|
|
+ generateEmployeeId,
|
|
|
+} from '@/data/mock';
|
|
|
+import { buildDeptTreeUnderLegal, pickLegalEntities } from '@/utils/organization-tree';
|
|
|
|
|
|
useAuthGuard();
|
|
|
|
|
|
@@ -128,120 +169,273 @@ const today = new Date();
|
|
|
const todayIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
|
|
|
|
const form = reactive({
|
|
|
+ createMode: 'manual',
|
|
|
+ validDays: 7,
|
|
|
name: '',
|
|
|
- department: '',
|
|
|
- position: '',
|
|
|
- source: '',
|
|
|
+ hireDate: '',
|
|
|
+ legalId: null,
|
|
|
+ legalName: '',
|
|
|
+ deptIds: [],
|
|
|
+ deptId: null,
|
|
|
+ deptPath: '',
|
|
|
+ positionId: null,
|
|
|
+ positionName: '',
|
|
|
phone: '',
|
|
|
email: '',
|
|
|
- expectedEntryDate: '',
|
|
|
+ idNumber: '',
|
|
|
+ employmentType: 'FULL_TIME',
|
|
|
+ probationFlag: 1,
|
|
|
probationMonths: 3,
|
|
|
- mentor: '',
|
|
|
remark: '',
|
|
|
});
|
|
|
|
|
|
+const companies = ref([]);
|
|
|
+const orgList = ref([]);
|
|
|
+const deptTree = ref([]);
|
|
|
+const deptIndex = ref([0, 0, 0]);
|
|
|
+
|
|
|
+const deptColumns = computed(() => {
|
|
|
+ const t = deptTree.value;
|
|
|
+ const l1 = t.map((n) => ({ name: n.name, id: n.id }));
|
|
|
+ const l2 = l1.length && deptIndex.value[0] < l1.length
|
|
|
+ ? (t[deptIndex.value[0]].children || []).map((n) => ({ name: n.name, id: n.id }))
|
|
|
+ : [];
|
|
|
+ const l3 = l2.length && deptIndex.value[1] < l2.length
|
|
|
+ ? (t[deptIndex.value[0]].children[deptIndex.value[1]].children || []).map((n) => ({ name: n.name, id: n.id }))
|
|
|
+ : [];
|
|
|
+ return [l1, l2, l3];
|
|
|
+});
|
|
|
+
|
|
|
+const positionKeyword = ref('');
|
|
|
+const positionResults = ref([]);
|
|
|
+const positionLoading = ref(false);
|
|
|
+const submitting = ref(false);
|
|
|
+const inviteResult = ref(null);
|
|
|
+const showInviteCard = ref(false);
|
|
|
+
|
|
|
const avatar = computed(() => {
|
|
|
const n = String(form.name || '').trim();
|
|
|
- return n ? n.charAt(0) : '新';
|
|
|
+ return n ? n.charAt(0) : (form.createMode === 'invite' ? '邀' : '新');
|
|
|
});
|
|
|
|
|
|
const subPreview = computed(() => {
|
|
|
const parts = [];
|
|
|
- if (form.department) parts.push(form.department);
|
|
|
- if (form.position) parts.push(form.position);
|
|
|
+ if (form.deptPath) parts.push(form.deptPath);
|
|
|
+ if (form.positionName) parts.push(form.positionName);
|
|
|
+ if (!parts.length && form.createMode === 'invite') parts.push('生成邀请链接');
|
|
|
return parts.length ? parts.join(' · ') : '请填写基础信息';
|
|
|
});
|
|
|
|
|
|
-function onDateChange(e) {
|
|
|
- form.expectedEntryDate = e.detail.value;
|
|
|
+const employmentLabel = computed(() => {
|
|
|
+ const hit = employmentTypes.find((e) => e.value === form.employmentType);
|
|
|
+ return hit ? hit.label : form.employmentType;
|
|
|
+});
|
|
|
+
|
|
|
+async function refreshCompanies() {
|
|
|
+ try {
|
|
|
+ // 一次拉全量(与 PC 端 onboardingHandling 一致),客户端拆分法人 + 部门树
|
|
|
+ orgList.value = await listOrganizations();
|
|
|
+ companies.value = pickLegalEntities(orgList.value);
|
|
|
+ } catch (err) {
|
|
|
+ console.error('[onboard-create] 拉取组织列表失败', err);
|
|
|
+ orgList.value = [];
|
|
|
+ companies.value = [];
|
|
|
+ uni.showToast({ title: '组织列表加载失败', icon: 'none' });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function refreshDeptTree(legalId) {
|
|
|
+ if (!legalId) {
|
|
|
+ deptTree.value = [];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ deptTree.value = buildDeptTreeUnderLegal(orgList.value, legalId);
|
|
|
+}
|
|
|
+
|
|
|
+function onHireDateChange(e) {
|
|
|
+ form.hireDate = e.detail.value;
|
|
|
+}
|
|
|
+
|
|
|
+function chooseLegal() {
|
|
|
+ if (!companies.value.length) {
|
|
|
+ uni.showToast({ title: '法人列表为空', icon: 'none' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ uni.showActionSheet({
|
|
|
+ itemList: companies.value.map((c) => c.name),
|
|
|
+ success: (r) => {
|
|
|
+ const c = companies.value[r.tapIndex];
|
|
|
+ form.legalId = c.id;
|
|
|
+ form.legalName = c.name;
|
|
|
+ form.company = c.name;
|
|
|
+ form.deptIds = [];
|
|
|
+ form.deptId = null;
|
|
|
+ form.deptPath = '';
|
|
|
+ deptIndex.value = [0, 0, 0];
|
|
|
+ refreshDeptTree(c.id);
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 部门字段点击:未选法人时弹提示并自动展开法人选择
|
|
|
+function onDeptFieldTap() {
|
|
|
+ if (!form.legalId) {
|
|
|
+ uni.showToast({ title: '请先选择法人', icon: 'none' });
|
|
|
+ chooseLegal();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onDeptColChange(e) {
|
|
|
+ const next = [...deptIndex.value];
|
|
|
+ next[e.detail.column] = e.detail.value;
|
|
|
+ if (e.detail.column === 0) {
|
|
|
+ next[1] = 0;
|
|
|
+ next[2] = 0;
|
|
|
+ }
|
|
|
+ if (e.detail.column === 1) {
|
|
|
+ next[2] = 0;
|
|
|
+ }
|
|
|
+ deptIndex.value = next;
|
|
|
+}
|
|
|
+
|
|
|
+function onDeptPick(e) {
|
|
|
+ const [i0, i1, i2] = e.detail.value;
|
|
|
+ const l1 = deptTree.value[i0]; // 法人节点(picker 第一栏显示用)
|
|
|
+ const l2 = l1?.children?.[i1]; // 一级部门
|
|
|
+ const l3 = l2?.children?.[i2]; // 二级部门
|
|
|
+ // deptIds / deptId 只保留部门节点,跳过根法人
|
|
|
+ const deptNodes = [l2, l3].filter(Boolean);
|
|
|
+ form.deptIds = deptNodes.map((n) => n.id);
|
|
|
+ form.deptId = form.deptIds[form.deptIds.length - 1] || null;
|
|
|
+ form.deptPath = deptNodes
|
|
|
+ .map((n) => n.name)
|
|
|
+ .filter(Boolean)
|
|
|
+ .join(' / ');
|
|
|
}
|
|
|
|
|
|
-function chooseDept() {
|
|
|
+function chooseEmployment() {
|
|
|
uni.showActionSheet({
|
|
|
- itemList: onboardOptions.departments,
|
|
|
- success: (r) => (form.department = onboardOptions.departments[r.tapIndex]),
|
|
|
+ itemList: employmentTypes.map((e) => e.label),
|
|
|
+ success: (r) => (form.employmentType = employmentTypes[r.tapIndex].value),
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-function chooseSource() {
|
|
|
+function chooseProbationMonths() {
|
|
|
uni.showActionSheet({
|
|
|
- itemList: onboardOptions.sources,
|
|
|
- success: (r) => (form.source = onboardOptions.sources[r.tapIndex]),
|
|
|
+ itemList: probationMonthOptions.map((n) => `${n} 个月`),
|
|
|
+ success: (r) => (form.probationMonths = probationMonthOptions[r.tapIndex]),
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-function chooseProbation() {
|
|
|
- const labels = onboardOptions.probationMonths.map((n) => `${n} 个月`);
|
|
|
+function chooseValidDays() {
|
|
|
uni.showActionSheet({
|
|
|
- itemList: labels,
|
|
|
- success: (r) => (form.probationMonths = onboardOptions.probationMonths[r.tapIndex]),
|
|
|
+ itemList: invitationValidDays.map((n) => `${n} 天`),
|
|
|
+ success: (r) => (form.validDays = invitationValidDays[r.tapIndex]),
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-function onCancel() {
|
|
|
- uni.showModal({
|
|
|
- title: '放弃创建',
|
|
|
- content: '当前填写的内容将不会保留,确定离开?',
|
|
|
+let positionDebounce = null;
|
|
|
+async function searchPositions(keyword) {
|
|
|
+ positionLoading.value = true;
|
|
|
+ try {
|
|
|
+ const data = await pagePosition({
|
|
|
+ pageNum: 1,
|
|
|
+ size: 50,
|
|
|
+ keyword: keyword || undefined,
|
|
|
+ positionName: keyword || undefined,
|
|
|
+ });
|
|
|
+ positionResults.value = (data?.list || []).map((p) => ({
|
|
|
+ id: p.id,
|
|
|
+ name: p.positionName,
|
|
|
+ deptName: p.deptName,
|
|
|
+ }));
|
|
|
+ } finally {
|
|
|
+ positionLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onPositionInput(e) {
|
|
|
+ positionKeyword.value = e.detail.value;
|
|
|
+ clearTimeout(positionDebounce);
|
|
|
+ positionDebounce = setTimeout(() => searchPositions(positionKeyword.value), 300);
|
|
|
+}
|
|
|
+
|
|
|
+function choosePosition() {
|
|
|
+ // 进入页面已经预拉过;这里只在第一次打开且空结果时补一次兜底
|
|
|
+ if (!positionResults.value.length) {
|
|
|
+ searchPositions(positionKeyword.value);
|
|
|
+ uni.showToast({ title: '暂无岗位数据', icon: 'none' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ uni.showActionSheet({
|
|
|
+ itemList: positionResults.value.map((p) => p.name),
|
|
|
success: (r) => {
|
|
|
- if (r.confirm) go('/pages/manage/index?type=onboard');
|
|
|
+ const p = positionResults.value[r.tapIndex];
|
|
|
+ form.positionId = p.id;
|
|
|
+ form.positionName = p.name;
|
|
|
+ positionKeyword.value = p.name;
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function validate() {
|
|
|
- const required = [
|
|
|
- ['name', '请填写姓名'],
|
|
|
- ['department', '请选择部门'],
|
|
|
- ['position', '请填写岗位'],
|
|
|
- ['source', '请选择招聘来源'],
|
|
|
- ['phone', '请填写手机号'],
|
|
|
- ['email', '请填写邮箱'],
|
|
|
- ['expectedEntryDate', '请选择预计入职日期'],
|
|
|
- ];
|
|
|
- for (const [key, msg] of required) {
|
|
|
- if (!form[key]) {
|
|
|
- uni.showToast({ title: msg, icon: 'none' });
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
- if (!/^1[3-9]\d{9}$/.test(form.phone)) {
|
|
|
- uni.showToast({ title: '手机号格式不正确', icon: 'none' });
|
|
|
- return false;
|
|
|
- }
|
|
|
- if (!/^[\w.+-]+@[\w-]+\.[\w.-]+$/.test(form.email)) {
|
|
|
- uni.showToast({ title: '邮箱格式不正确', icon: 'none' });
|
|
|
- return false;
|
|
|
- }
|
|
|
- return true;
|
|
|
-}
|
|
|
-
|
|
|
-function buildOnboarding() {
|
|
|
- const id = generateEmployeeId(mergeOnboardings().map((o) => o.id));
|
|
|
- const [yyyy, mm, dd] = form.expectedEntryDate.split('-');
|
|
|
- const expected = form.expectedEntryDate;
|
|
|
- const probationEnd = form.probationMonths
|
|
|
- ? `${yyyy}-${mm}-${String(Number(dd)).padStart(2, '0')}`.replace(/^(\d{4})-(\d{2})-(\d{2})$/, (_m, y, m, d) => {
|
|
|
- const dt = new Date(Number(y), Number(m) - 1, Number(d));
|
|
|
- dt.setMonth(dt.getMonth() + Number(form.probationMonths));
|
|
|
- return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`;
|
|
|
- })
|
|
|
+ if (form.createMode === 'manual' && !form.name.trim()) return '请填写姓名';
|
|
|
+ if (!form.hireDate) return '请选择入职日期';
|
|
|
+ if (!form.legalId) return '请选择所属法人';
|
|
|
+ if (!form.deptId) return '请选择目标部门';
|
|
|
+ if (!form.positionId) return '请选择岗位';
|
|
|
+ if (form.phone && !/^1[3-9]\d{9}$/.test(form.phone)) return '手机号格式不正确';
|
|
|
+ if (form.email && !/^[\w.+-]+@[\w-]+\.[\w.-]+$/.test(form.email)) return '邮箱格式不正确';
|
|
|
+ if (form.idNumber && !/^\d{17}[\dXx]$/.test(form.idNumber)) return '身份证号格式不正确';
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+function buildLocalOnboarding(onboardingId) {
|
|
|
+ const d = new Date(form.hireDate);
|
|
|
+ const probationEnd = form.probationFlag === 1 && form.probationMonths
|
|
|
+ ? new Date(d.getFullYear(), d.getMonth() + Number(form.probationMonths), d.getDate())
|
|
|
: null;
|
|
|
return {
|
|
|
- id,
|
|
|
+ id: onboardingId,
|
|
|
+ onboardingId,
|
|
|
+ employeeNo: onboardingId,
|
|
|
+ invitationId: '',
|
|
|
+ invitationNo: '',
|
|
|
+ avatar: (form.name || '新').trim().slice(0, 1),
|
|
|
name: form.name.trim(),
|
|
|
- avatar: form.name.trim().charAt(0),
|
|
|
- department: form.department,
|
|
|
- position: form.position.trim(),
|
|
|
- source: form.source,
|
|
|
+ company: form.legalName,
|
|
|
+ legalId: form.legalId,
|
|
|
+ department: form.deptPath,
|
|
|
+ deptId: form.deptId,
|
|
|
+ position: form.positionName,
|
|
|
+ positionId: form.positionId,
|
|
|
+ source: '社会招聘',
|
|
|
status: 'pending',
|
|
|
- expectedEntryDate: expected,
|
|
|
+ expectedEntryDate: form.hireDate,
|
|
|
entryDate: null,
|
|
|
probationMonths: Number(form.probationMonths) || 3,
|
|
|
- probationEndDate: probationEnd,
|
|
|
- mentor: form.mentor.trim() || '待安排',
|
|
|
+ probationFlag: Number(form.probationFlag) || 1,
|
|
|
+ probationEndDate: probationEnd
|
|
|
+ ? `${probationEnd.getFullYear()}-${String(probationEnd.getMonth() + 1).padStart(2, '0')}-${String(probationEnd.getDate()).padStart(2, '0')}`
|
|
|
+ : null,
|
|
|
+ mentor: '待安排',
|
|
|
phone: form.phone.trim(),
|
|
|
email: form.email.trim(),
|
|
|
+ idNumber: form.idNumber.trim(),
|
|
|
+ gender: '',
|
|
|
+ age: '',
|
|
|
+ employmentType: form.employmentType,
|
|
|
+ validDays: form.validDays,
|
|
|
+ createMode: form.createMode,
|
|
|
+ flow: '待补充档案',
|
|
|
+ offer: '未发起',
|
|
|
+ contract: '未签署',
|
|
|
+ assets: '未领用',
|
|
|
remark: form.remark.trim(),
|
|
|
+ publicPath: '',
|
|
|
+ publicToken: '',
|
|
|
+ invitationStatus: '',
|
|
|
+ rowType: 'record',
|
|
|
materials: {
|
|
|
identityCard: false,
|
|
|
degreeCertificate: false,
|
|
|
@@ -250,7 +444,7 @@ function buildOnboarding() {
|
|
|
bankCard: false,
|
|
|
resignationCert: false,
|
|
|
},
|
|
|
- contract: { status: '待签订', signMethod: '电子签', deadline: '' },
|
|
|
+ contractDetail: { status: '待签订', signMethod: '电子签', deadline: form.hireDate },
|
|
|
socialSecurity: { status: '未办理', city: '北京', deadline: '' },
|
|
|
training: { status: '未开始', name: '新员工入职培训', duration: '2天', deadline: '' },
|
|
|
workstation: { status: '待分配', location: '' },
|
|
|
@@ -260,18 +454,149 @@ function buildOnboarding() {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-function onSubmit() {
|
|
|
- if (!validate()) return;
|
|
|
- const staff = buildOnboarding();
|
|
|
- addOnboarding(staff);
|
|
|
- uni.showToast({ title: '入职流程已创建', icon: 'success' });
|
|
|
- setTimeout(() => {
|
|
|
- go(`/pages/manage/onboard-detail?id=${staff.id}`);
|
|
|
- }, 600);
|
|
|
+async function onSubmit() {
|
|
|
+ const err = validate();
|
|
|
+ if (err) {
|
|
|
+ uni.showToast({ title: err, icon: 'none' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ submitting.value = true;
|
|
|
+ try {
|
|
|
+ if (form.createMode === 'manual') {
|
|
|
+ console.log('manual', form);
|
|
|
+ const payload = {
|
|
|
+ name: form.name.trim(),
|
|
|
+ hireDate: form.hireDate,
|
|
|
+ legalId: form.legalId,
|
|
|
+ deptId: form.deptId,
|
|
|
+ positionId: form.positionId,
|
|
|
+ positionName: form.positionName,
|
|
|
+ phone: form.phone.trim(),
|
|
|
+ email: form.email.trim(),
|
|
|
+ idNumber: form.idNumber.trim(),
|
|
|
+ employmentType: form.employmentType,
|
|
|
+ probationFlag: form.probationFlag,
|
|
|
+ probationMonths: form.probationMonths,
|
|
|
+ remark: form.remark.trim(),
|
|
|
+ };
|
|
|
+ const { onboardingId } = await createOnboardingManual(payload);
|
|
|
+ const id = onboardingId || generateEmployeeId(mergeOnboardings().map((o) => o.id));
|
|
|
+ addOnboarding(buildLocalOnboarding(id));
|
|
|
+ clearOnboardingDraft();
|
|
|
+ uni.showToast({ title: '入职流程已创建', icon: 'success' });
|
|
|
+ setTimeout(() => go(`/pages/manage/onboard-detail?id=${id}`), 500);
|
|
|
+ } else {
|
|
|
+ const payload = {
|
|
|
+ name: form.name.trim(),
|
|
|
+ hireDate: form.hireDate,
|
|
|
+ legalId: form.legalId,
|
|
|
+ deptId: form.deptId,
|
|
|
+ positionId: form.positionId,
|
|
|
+ positionName: form.positionName,
|
|
|
+ employmentType: form.employmentType,
|
|
|
+ probationFlag: form.probationFlag,
|
|
|
+ probationMonths: form.probationMonths,
|
|
|
+ remark: form.remark.trim(),
|
|
|
+ };
|
|
|
+ const result = await createOnboardingInvitation(payload, form.validDays);
|
|
|
+ addOnboardingInvitation({
|
|
|
+ invitationId: result.invitationId,
|
|
|
+ publicToken: result.publicToken,
|
|
|
+ publicPath: result.publicPath,
|
|
|
+ onboardingId: result.onboardingId || '',
|
|
|
+ name: form.name.trim(),
|
|
|
+ department: form.deptPath,
|
|
|
+ position: form.positionName,
|
|
|
+ legalId: form.legalId,
|
|
|
+ employmentType: form.employmentType,
|
|
|
+ offer: '待填写',
|
|
|
+ invitationStatus: 'PENDING',
|
|
|
+ validDays: form.validDays,
|
|
|
+ expiresAt: (() => {
|
|
|
+ const d = new Date();
|
|
|
+ d.setDate(d.getDate() + form.validDays);
|
|
|
+ return d.toISOString();
|
|
|
+ })(),
|
|
|
+ createdAt: new Date().toISOString(),
|
|
|
+ });
|
|
|
+ inviteResult.value = result;
|
|
|
+ showInviteCard.value = true;
|
|
|
+ uni.showToast({ title: '邀请已创建', icon: 'success' });
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e?.message || '提交失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ submitting.value = false;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+function copyInviteLink() {
|
|
|
+ const url = inviteResult.value?.publicPath;
|
|
|
+ if (!url) return;
|
|
|
+ uni.setClipboardData({
|
|
|
+ data: url,
|
|
|
+ success: () => uni.showToast({ title: '链接已复制', icon: 'success' }),
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function onCancel() {
|
|
|
+ uni.showModal({
|
|
|
+ title: '放弃创建',
|
|
|
+ content: '当前填写的内容将不会保留,确定离开?',
|
|
|
+ success: (r) => {
|
|
|
+ if (r.confirm) go('/pages/manage/index?type=onboard');
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 新建入口:不保存草稿、不恢复草稿,刷新或重进都是干净状态
|
|
|
onLoad(() => {
|
|
|
uni.setNavigationBarTitle({ title: '新建入职流程' });
|
|
|
+ refreshCompanies();
|
|
|
+ searchPositions(''); // 进入即拉岗位列表,避免点开岗位栏才加载的等待感
|
|
|
+ clearOnboardingDraft();
|
|
|
+});
|
|
|
+
|
|
|
+// 每次进入页面都重新拉取,避免服务器/演示模式切换后列表为空
|
|
|
+onShow(() => {
|
|
|
+ if (!companies.value.length) refreshCompanies();
|
|
|
+});
|
|
|
+
|
|
|
+// 离开页面:清空草稿 + 重置内存表单,下次进入是干净状态
|
|
|
+function resetForm() {
|
|
|
+ Object.assign(form, {
|
|
|
+ createMode: 'manual',
|
|
|
+ validDays: 7,
|
|
|
+ name: '',
|
|
|
+ hireDate: '',
|
|
|
+ legalId: null,
|
|
|
+ legalName: '',
|
|
|
+ deptIds: [],
|
|
|
+ deptId: null,
|
|
|
+ deptPath: '',
|
|
|
+ positionId: null,
|
|
|
+ positionName: '',
|
|
|
+ phone: '',
|
|
|
+ email: '',
|
|
|
+ idNumber: '',
|
|
|
+ employmentType: 'FULL_TIME',
|
|
|
+ probationFlag: 1,
|
|
|
+ probationMonths: 3,
|
|
|
+ remark: '',
|
|
|
+ });
|
|
|
+ positionKeyword.value = '';
|
|
|
+ positionResults.value = [];
|
|
|
+ inviteResult.value = null;
|
|
|
+ showInviteCard.value = false;
|
|
|
+ orgList.value = [];
|
|
|
+ companies.value = [];
|
|
|
+ deptTree.value = [];
|
|
|
+ deptIndex.value = [0, 0, 0];
|
|
|
+}
|
|
|
+
|
|
|
+onUnload(() => {
|
|
|
+ clearOnboardingDraft();
|
|
|
+ resetForm();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
@@ -283,6 +608,7 @@ onLoad(() => {
|
|
|
color: #fff;
|
|
|
background: linear-gradient(145deg, #4ab8e8, #1677ff);
|
|
|
}
|
|
|
+
|
|
|
.head-avatar {
|
|
|
width: 88rpx;
|
|
|
height: 88rpx;
|
|
|
@@ -295,104 +621,128 @@ onLoad(() => {
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
+
|
|
|
.head-info {
|
|
|
flex: 1;
|
|
|
margin-left: 20rpx;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
+
|
|
|
.head-name {
|
|
|
display: block;
|
|
|
font-size: 32rpx;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
+
|
|
|
.head-sub {
|
|
|
display: block;
|
|
|
margin-top: 8rpx;
|
|
|
font-size: 22rpx;
|
|
|
opacity: 0.86;
|
|
|
}
|
|
|
-.steps {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- padding: 24rpx 28rpx;
|
|
|
- background: #fff;
|
|
|
- border-bottom: 1rpx solid #edf0f2;
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ padding: 32rpx 28rpx 18rpx;
|
|
|
}
|
|
|
-.step {
|
|
|
+
|
|
|
+.mode-row {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- font-size: 22rpx;
|
|
|
- color: #8b939f;
|
|
|
+ gap: 18rpx;
|
|
|
+ padding: 0 24rpx;
|
|
|
}
|
|
|
-.step view {
|
|
|
- width: 38rpx;
|
|
|
- height: 38rpx;
|
|
|
- border-radius: 50%;
|
|
|
- background: #edf0f2;
|
|
|
- color: #8b939f;
|
|
|
- font-size: 22rpx;
|
|
|
- font-weight: 600;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- margin-right: 8rpx;
|
|
|
+
|
|
|
+.radio-card {
|
|
|
+ flex: 1;
|
|
|
+ display: inline-flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 24rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ border: 2rpx solid #edf0f2;
|
|
|
+ background: #fff;
|
|
|
+ color: #4b5563;
|
|
|
}
|
|
|
-.step.active text {
|
|
|
+
|
|
|
+.radio-card.is-checked {
|
|
|
+ border-color: #1677ff;
|
|
|
+ background: #e7f0ff;
|
|
|
color: #1677ff;
|
|
|
+}
|
|
|
+
|
|
|
+.mode-title {
|
|
|
+ font-size: 28rpx;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
-.step.active view {
|
|
|
- background: #1677ff;
|
|
|
- color: #fff;
|
|
|
+
|
|
|
+.mode-desc {
|
|
|
+ margin-top: 8rpx;
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #8b939f;
|
|
|
}
|
|
|
-.step-line {
|
|
|
- flex: 1;
|
|
|
- height: 2rpx;
|
|
|
- background: #edf0f2;
|
|
|
- margin: 0 12rpx;
|
|
|
+
|
|
|
+.radio-card.is-checked .mode-desc {
|
|
|
+ color: #1677ff;
|
|
|
+ opacity: 0.8;
|
|
|
}
|
|
|
+
|
|
|
.form-card {
|
|
|
padding: 4rpx 28rpx;
|
|
|
}
|
|
|
+
|
|
|
.field {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
min-height: 96rpx;
|
|
|
font-size: 28rpx;
|
|
|
}
|
|
|
+
|
|
|
.field .label {
|
|
|
width: 180rpx;
|
|
|
color: #4b5563;
|
|
|
}
|
|
|
+
|
|
|
.field .label.required::before {
|
|
|
content: '*';
|
|
|
color: #ee4d4d;
|
|
|
margin-right: 4rpx;
|
|
|
}
|
|
|
+
|
|
|
.field .value,
|
|
|
-.field input {
|
|
|
+.field input,
|
|
|
+.field picker,
|
|
|
+.field .placeholder {
|
|
|
flex: 1;
|
|
|
text-align: right;
|
|
|
color: #1f2329;
|
|
|
font-size: 28rpx;
|
|
|
}
|
|
|
+
|
|
|
+.field .placeholder {
|
|
|
+ color: #b5bac3;
|
|
|
+}
|
|
|
+
|
|
|
.value-input {
|
|
|
text-align: right;
|
|
|
height: 96rpx;
|
|
|
}
|
|
|
+
|
|
|
.placeholder {
|
|
|
color: #b5bac3;
|
|
|
}
|
|
|
+
|
|
|
.arrow {
|
|
|
margin-left: 12rpx;
|
|
|
color: #c4c8ce;
|
|
|
font-size: 32rpx;
|
|
|
}
|
|
|
+
|
|
|
.divider {
|
|
|
height: 1rpx;
|
|
|
background: #edf0f2;
|
|
|
margin-left: 180rpx;
|
|
|
}
|
|
|
+
|
|
|
.textarea {
|
|
|
width: 100%;
|
|
|
min-height: 180rpx;
|
|
|
@@ -401,6 +751,7 @@ onLoad(() => {
|
|
|
color: #1f2329;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
+
|
|
|
.textarea-count {
|
|
|
display: block;
|
|
|
text-align: right;
|
|
|
@@ -408,17 +759,70 @@ onLoad(() => {
|
|
|
color: #8b939f;
|
|
|
padding-bottom: 24rpx;
|
|
|
}
|
|
|
+
|
|
|
+.inline-radios {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 14rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.inline-radios .radio-card {
|
|
|
+ flex: none;
|
|
|
+ padding: 12rpx 28rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+ flex-direction: row;
|
|
|
+}
|
|
|
+
|
|
|
+.invite-card {
|
|
|
+ padding: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.invite-head {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.invite-head>text:first-child {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.invite-url {
|
|
|
+ font-family: monospace;
|
|
|
+ word-break: break-all;
|
|
|
+ background: #f7f8fa;
|
|
|
+ padding: 16rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ margin: 16rpx 0;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #4b5563;
|
|
|
+}
|
|
|
+
|
|
|
+.invite-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 18rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.invite-actions>view {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
.footer-tip {
|
|
|
padding: 28rpx 28rpx 8rpx;
|
|
|
font-size: 22rpx;
|
|
|
text-align: center;
|
|
|
}
|
|
|
+
|
|
|
.footer-actions {
|
|
|
display: flex;
|
|
|
gap: 18rpx;
|
|
|
padding: 24rpx 24rpx calc(40rpx + env(safe-area-inset-bottom));
|
|
|
}
|
|
|
-.footer-actions > view {
|
|
|
+
|
|
|
+.footer-actions>view {
|
|
|
flex: 1;
|
|
|
}
|
|
|
</style>
|