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