Explorar o código

feat(onboard): 重写新建入职页 — 办理方式/法人/三级部门/岗位搜索/用工类型与字段右对齐

xieyong hai 4 días
pai
achega
afe5946481
Modificáronse 1 ficheiros con 587 adicións e 183 borrados
  1. 587 183
      src/pages/manage/onboard-create.vue

+ 587 - 183
src/pages/manage/onboard-create.vue

@@ -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>