소스 검색

feat(onboard): 详情页新增 5 步办理网格 + 邀请链接卡 + 提交审批

xieyong 4 일 전
부모
커밋
e6565282c1
1개의 변경된 파일255개의 추가작업 그리고 9개의 파일을 삭제
  1. 255 9
      src/pages/manage/onboard-detail.vue

+ 255 - 9
src/pages/manage/onboard-detail.vue

@@ -66,6 +66,51 @@
       </view>
     </view>
 
+    <!-- 办理步骤 -->
+    <view class="section-title"
+      ><text>办理步骤</text
+      ><text class="section-more">{{ stepsDone }} / {{ onboardingStepDefs.length }} 已完成</text></view
+    >
+    <view class="step-grid card section">
+      <view
+        v-for="(s, idx) in onboardingStepDefs"
+        :key="s.key"
+        class="step-row"
+        :class="{
+          'is-done': stepDone(s.key),
+          'is-current': currentStep === s.key && !stepDone(s.key),
+          'is-pending': !stepDone(s.key) && currentStep !== s.key,
+        }"
+      >
+        <view class="step-node">
+          <view class="step-dot">{{ s.icon }}</view>
+          <view v-if="idx !== onboardingStepDefs.length - 1" class="step-line"></view>
+        </view>
+        <view class="step-main">
+          <text class="step-label">{{ s.label }}</text>
+          <text class="step-desc">{{ stepDesc(s.key) }}</text>
+        </view>
+        <text v-if="stepDone(s.key)" class="status-tag status-approved">已完成</text>
+        <text v-else-if="currentStep === s.key" class="status-tag status-processing">进行中</text>
+        <text v-else class="status-tag status-pending">待开始</text>
+      </view>
+    </view>
+
+    <!-- 邀请链接(仅邀请创建时)-->
+    <view v-if="staff.publicToken" class="section">
+      <view class="invite-card card">
+        <view class="invite-head">
+          <text>邀请链接</text>
+          <text class="status-tag status-approved">邀请方式</text>
+        </view>
+        <text class="muted">将以下链接发送给候选人,引导其填写档案资料。</text>
+        <view class="invite-url">{{ staff.publicPath }}</view>
+        <view class="invite-actions">
+          <view class="primary-btn" @click="copyInviteLink">复制链接</view>
+        </view>
+      </view>
+    </view>
+
     <!-- 入职材料清单 -->
     <view class="section-title"
       ><text>入职材料</text
@@ -138,12 +183,12 @@
       <view class="info-card card">
         <view class="info-head"
           ><text class="info-title">合同签订</text
-          ><text class="status-tag" :class="moduleClass(staff.contract.status)">{{ staff.contract.status }}</text></view
+          ><text class="status-tag" :class="moduleClass(staff.contractDetail?.status)">{{ staff.contractDetail?.status || '—' }}</text></view
         >
-        <view class="info-row"><text>签署方式</text><text>{{ staff.contract.signMethod || '—' }}</text></view>
+        <view class="info-row"><text>签署方式</text><text>{{ staff.contractDetail?.signMethod || '—' }}</text></view>
         <view class="info-row"
-          ><text>{{ staff.contract.signDate ? '签订日期' : '截止日期' }}</text
-          ><text>{{ staff.contract.signDate || staff.contract.deadline || '—' }}</text
+          ><text>{{ staff.contractDetail?.signDate ? '签订日期' : '截止日期' }}</text
+          ><text>{{ staff.contractDetail?.signDate || staff.contractDetail?.deadline || '—' }}</text
         ></view
         >
       </view>
@@ -208,7 +253,17 @@
 
     <view class="footer-actions">
       <view class="ghost-btn" @click="onRemind">催办</view>
-      <view class="primary-btn" @click="onConfirm">确认入职</view>
+      <view
+        v-if="canSubmitApproval"
+        class="primary-btn"
+        @click="onSubmitApproval"
+      >提交审批</view>
+      <view
+        v-else-if="staff.status !== 'done' && currentStep !== 'complete'"
+        class="primary-btn"
+        @click="onAdvance({ key: currentStep })"
+      >推进当前步骤</view>
+      <view v-else class="primary-btn" @click="onConfirm">确认完成</view>
     </view>
   </view>
 </template>
@@ -217,7 +272,7 @@
 import { computed, ref, watch } from "vue";
 import { onLoad } from "@dcloudio/uni-app";
 import { useAuthGuard } from "@/hooks/useAuthGuard";
-import { onboardings, onboardStatus, onboardStages } from "@/data/mock";
+import { onboardings, onboardStatus, onboardStages, onboardingStepDefs, onboardingStepText } from "@/data/mock";
 import { go } from "@/utils/router";
 import {
   getOnboardMaterials,
@@ -229,7 +284,9 @@ import {
   formatNow,
   getOnboardingById,
   mergeOnboardings,
+  updateOnboarding,
 } from "@/utils/storage";
+import { submitOnboardingRecord } from "@/api/onboarding";
 
 useAuthGuard();
 
@@ -482,17 +539,105 @@ function fileDisplayUrl(file) {
 
 function onConfirm() {
   uni.showModal({
-    title: "确认入职",
-    content: "演示版:将把该员工标记为已入职状态(实际不会修改 mock 数据)。",
+    title: "完成入职",
+    content: `将「${staff.value.name}」标记为已完成入职?`,
     success: (r) => {
       if (r.confirm) {
-        uni.showToast({ title: "操作成功", icon: "success" });
+        updateOnboarding(staff.value.id, {
+          status: "done",
+          contract: "已签署",
+          assets: "已领用",
+          flow: "档案已完善",
+          offer: "已发起",
+        });
+        uni.showToast({ title: "已完成", icon: "success" });
         setTimeout(() => go("/pages/manage/index?type=onboard"), 600);
       }
     },
   });
 }
 
+function stepDone(k) {
+  const r = staff.value;
+  return ({
+    offer: !!r.offer && r.offer !== "未发起" && r.offer !== "待填写",
+    profile: r.flow === "档案已完善",
+    contract: ["审批中", "已通过", "已签署"].includes(r.contract),
+    assets: r.assets === "已领用",
+    complete: r.status === "done",
+  })[k];
+}
+
+function nextStepKey(r) {
+  if (!r.offer || r.offer === "未发起" || r.offer === "待填写") return "offer";
+  if (r.flow !== "档案已完善") return "profile";
+  if (!r.contract || ["未发起", "未签署", "待签署"].includes(r.contract)) return "contract";
+  if (r.assets !== "已领用") return "assets";
+  return "complete";
+}
+
+const currentStep = computed(() => nextStepKey(staff.value));
+const stepsDone = computed(() => onboardingStepDefs.filter((s) => stepDone(s.key)).length);
+const canSubmitApproval = computed(
+  () => Boolean(staff.value.onboardingId) && staff.value.status !== "done",
+);
+
+function stepDesc(k) {
+  const r = staff.value;
+  return ({
+    offer: r.offer || onboardingStepText.offer.empty,
+    profile: r.flow || onboardingStepText.profile.empty,
+    contract: r.contract || onboardingStepText.contract.empty,
+    assets: r.assets || onboardingStepText.assets.empty,
+    complete:
+      r.status === "done" ? onboardingStepText.complete.done : onboardingStepText.complete.empty,
+  })[k];
+}
+
+async function onAdvance(step) {
+  const r = staff.value;
+  if (!r) return;
+  if (r.status === "done") return;
+  if (step.key === "contract" && r.onboardingId) return onSubmitApproval();
+  if (step.key === "assets" || step.key === "complete") {
+    return uni.showToast({ title: "演示版:当前步骤仅作展示", icon: "none" });
+  }
+  const patch = {};
+  if (step.key === "offer") patch.offer = "已发起";
+  if (step.key === "profile") patch.flow = "档案已完善";
+  const anyDone =
+    (patch.offer || r.offer) === "已发起" ||
+    patch.flow === "档案已完善" ||
+    ["已签署", "审批中", "已通过"].includes(r.contract) ||
+    r.assets === "已领用";
+  patch.status = anyDone ? "processing" : "pending";
+  updateOnboarding(r.id, patch);
+  uni.showToast({ title: "已推进到下一步", icon: "success" });
+  setTimeout(() => uni.reLaunch({ url: `/pages/manage/onboard-detail?id=${r.id}` }), 400);
+}
+
+async function onSubmitApproval() {
+  const r = staff.value;
+  if (!r.onboardingId) return uni.showToast({ title: "缺少入职单主键", icon: "none" });
+  try {
+    await submitOnboardingRecord(r.onboardingId);
+    updateOnboarding(r.id, { contract: "审批中", status: "processing" });
+    uni.showToast({ title: "已提交审批", icon: "success" });
+    setTimeout(() => uni.reLaunch({ url: `/pages/manage/onboard-detail?id=${r.id}` }), 400);
+  } catch (e) {
+    uni.showToast({ title: e?.message || "提交失败", icon: "none" });
+  }
+}
+
+function copyInviteLink() {
+  const url = staff.value.publicPath;
+  if (!url) return;
+  uni.setClipboardData({
+    data: url,
+    success: () => uni.showToast({ title: "链接已复制", icon: "success" }),
+  });
+}
+
 onLoad((options) => {
   id.value = options?.id || "";
   uni.setNavigationBarTitle({ title: "入职详情" });
@@ -849,4 +994,105 @@ onLoad((options) => {
 .footer-actions > view {
   flex: 1;
 }
+.step-grid {
+  padding: 8rpx 0;
+}
+.step-row {
+  display: flex;
+  align-items: flex-start;
+  padding: 18rpx 28rpx;
+}
+.step-node {
+  width: 36rpx;
+  position: relative;
+  padding-top: 12rpx;
+}
+.step-dot {
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 50%;
+  background: #c4c8ce;
+  color: #fff;
+  font-size: 22rpx;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin: 0 auto;
+}
+.step-line {
+  position: absolute;
+  top: 50rpx;
+  left: 50%;
+  bottom: -28rpx;
+  width: 2rpx;
+  background: #edf0f2;
+  transform: translateX(-50%);
+}
+.step-main {
+  flex: 1;
+  margin-left: 18rpx;
+}
+.step-label {
+  display: block;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #4b5563;
+}
+.step-desc {
+  display: block;
+  margin-top: 4rpx;
+  font-size: 22rpx;
+  color: #8b939f;
+}
+.step-row > .status-tag {
+  margin-top: 6rpx;
+  flex-shrink: 0;
+}
+.step-row.is-done .step-dot {
+  background: #52c41a;
+}
+.step-row.is-done .step-label {
+  color: #52c41a;
+}
+.step-row.is-current .step-dot {
+  background: #1677ff;
+  box-shadow: 0 0 0 6rpx rgba(22, 119, 255, 0.16);
+}
+.step-row.is-current .step-label {
+  color: #1677ff;
+}
+.step-row.is-pending .step-dot {
+  background: #c4c8ce;
+}
+.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;
+}
 </style>