From 40010b5668c0335e3c88a33ae600b3e3ebf43429 Mon Sep 17 00:00:00 2001 From: fuenmao <980740792@qq.com> Date: Mon, 14 Sep 2026 16:45:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=91=98=E5=B7=A5=E5=85=A5=E8=81=8C=E5=A1=AB?= =?UTF-8?q?=E5=86=99=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/index.ts | 41 +++++++++++ src/utils/onboardingValidation.ts | 10 +++ src/views/biz/onboarding/contract/index.vue | 11 ++- src/views/biz/onboarding/task/index.vue | 13 ++-- src/views/onboarding/fill/index.vue | 80 +++++++++++++++++---- 5 files changed, 133 insertions(+), 22 deletions(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index 5419e1a..8a09dc9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -323,3 +323,44 @@ export function findItemNested(enumData: any, callValue: any, value: string, chi if (current[children]) return findItemNested(current[children], callValue, value, children); }, null); } + +/** + * @description 复制文本到剪贴板(兼容安全上下文 HTTPS 与非安全上下文 HTTP/IP 环境) + * @param {String} text 要复制的文本 + * @returns {Promise} 是否成功复制 + */ +export async function copyToClipboard(text: string): Promise { + if (!text) return false; + + // 1. 现代 Clipboard API (仅在 Secure Context 如 HTTPS 或 localhost 下可用) + if (navigator?.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch (err) { + console.warn("navigator.clipboard.writeText failed, falling back to execCommand: ", err); + } + } + + // 2. 降级方案:创建隐藏 textarea 并调用 document.execCommand('copy') + try { + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.top = "0"; + textArea.style.left = "-9999px"; + textArea.style.opacity = "0"; + textArea.setAttribute("readonly", ""); + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + textArea.setSelectionRange(0, text.length); + + const successful = document.execCommand("copy"); + document.body.removeChild(textArea); + return successful; + } catch (fallbackErr) { + console.error("document.execCommand('copy') failed: ", fallbackErr); + return false; + } +} diff --git a/src/utils/onboardingValidation.ts b/src/utils/onboardingValidation.ts index 8c37f65..ed501c5 100644 --- a/src/utils/onboardingValidation.ts +++ b/src/utils/onboardingValidation.ts @@ -73,6 +73,16 @@ export function getIdCardBirthday(value: unknown) { return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`; } +export function getIdCardGender(value: unknown) { + const idCardNumber = normalizeIdCardNumber(value); + if (!/^\d{17}[0-9X]$/.test(idCardNumber)) { + return ""; + } + + const genderCode = Number(idCardNumber.charAt(16)); + return genderCode % 2 === 1 ? "男" : "女"; +} + export function isNumberInRange(value: unknown, min: number, max: number) { const text = String(value ?? "").trim(); if (!/^\d+(\.\d+)?$/.test(text)) { diff --git a/src/views/biz/onboarding/contract/index.vue b/src/views/biz/onboarding/contract/index.vue index 6043290..6074079 100644 --- a/src/views/biz/onboarding/contract/index.vue +++ b/src/views/biz/onboarding/contract/index.vue @@ -542,6 +542,7 @@ import { import type { ColumnProps, ProTableInstance } from "@/components/ProTable/interface"; import { isMessageBoxConfirmed } from "@/utils/messageBox"; import { useOnboardingPermission } from "@/utils/onboardingPermission"; +import { copyToClipboard } from "@/utils"; const { canManage, disabledReason } = useOnboardingPermission("onboardingContractManageAll"); @@ -997,9 +998,13 @@ function formatPlaceholder(code: string) { return `{{${code}}}`; } -function copyPlaceholder(code: string) { - navigator.clipboard.writeText(`{{${code}}}`); - ElMessage.success(`占位符 {{${code}}} 已复制到剪贴板`); +async function copyPlaceholder(code: string) { + const ok = await copyToClipboard(`{{${code}}}`); + if (ok) { + ElMessage.success(`占位符 {{${code}}} 已复制到剪贴板`); + } else { + ElMessage.warning(`自动复制失败,请手动复制 {{${code}}}`); + } } async function openVariables(row: OnboardingContractVersion) { diff --git a/src/views/biz/onboarding/task/index.vue b/src/views/biz/onboarding/task/index.vue index d483950..4e1d3e2 100644 --- a/src/views/biz/onboarding/task/index.vue +++ b/src/views/biz/onboarding/task/index.vue @@ -1509,6 +1509,7 @@ import { import type { ColumnProps, ProTableInstance } from "@/components/ProTable/interface"; import { getMessageBoxPromptResult, isMessageBoxConfirmed } from "@/utils/messageBox"; import { useOnboardingPermission } from "@/utils/onboardingPermission"; +import { copyToClipboard } from "@/utils"; const { canManage, disabledReason } = useOnboardingPermission("onboardingTaskManageAll"); @@ -2514,10 +2515,14 @@ async function createTask() { } } -function copyText(text?: string, successMsg = "复制成功") { +async function copyText(text?: string, successMsg = "复制成功") { if (!text) return; - navigator.clipboard.writeText(text); - ElMessage.success(successMsg); + const ok = await copyToClipboard(text); + if (ok) { + ElMessage.success(successMsg); + } else { + ElMessage.warning("自动复制失败,请在弹窗中长按或手动复制"); + } } function copyInviteMessage() { @@ -2543,7 +2548,7 @@ async function copyOrShareTaskLink(row: OnboardingTask) { row.linkExpireAt = data.expiresAt; proTable.value?.refresh(); - copyText(linkUrl, "专属入职链接已复制到剪贴板!"); + await copyText(linkUrl, "专属入职链接已复制到剪贴板!"); shareVisible.value = true; } catch (err: any) { ElMessage.error(err?.message || "获取专属链接失败"); diff --git a/src/views/onboarding/fill/index.vue b/src/views/onboarding/fill/index.vue index a30409f..ab594c0 100644 --- a/src/views/onboarding/fill/index.vue +++ b/src/views/onboarding/fill/index.vue @@ -115,6 +115,23 @@ + + + + + + + + + + + + + @@ -128,19 +145,13 @@ - - - - - - + @@ -151,7 +162,6 @@ - @@ -167,12 +177,6 @@ - - - - - @@ -683,6 +687,7 @@ import { } from "@/api/modules/biz/onboarding"; import { getIdCardBirthday, + getIdCardGender, getIdCardValidationMessage, isBankCardNumber, isContactPhone, @@ -878,6 +883,33 @@ function validateBirthday(_rule: any, value: unknown, callback: (error?: Error) callback(); } +// 监听身份证号码输入,自动提取出生年月日并联动推导性别 +watch( + () => form.idCardNumber, + (newVal) => { + const rawIdCard = normalizeIdCardNumber(newVal); + if (!rawIdCard) { + form.birthday = ""; + return; + } + const birthday = getIdCardBirthday(rawIdCard); + if (birthday) { + form.birthday = birthday; + // 联动推导性别(若尚未手动选择过) + const gender = getIdCardGender(rawIdCard); + if (gender && (!form.gender || form.gender === "男" || form.gender === "女")) { + form.gender = gender; + } + nextTick(() => { + regFormRef.value?.clearValidate(["birthday", "gender"]); + }); + } else if (rawIdCard.length < 18) { + form.birthday = ""; + } + }, + { immediate: true } +); + function validateBankCardNumber(_rule: any, value: unknown, callback: (error?: Error) => void) { if (value && !isBankCardNumber(value)) { return callback(new Error("请输入12至19位银行卡号")); @@ -963,9 +995,12 @@ const regRules = computed(() => { } const useChangeTrigger = changeTriggerFields.has(fieldKey); + const message = fieldKey === "birthday" + ? "请先输入正确的身份证号码以生成出生日期" + : `${useChangeTrigger ? "请选择" : "请填写"}${requirement.fieldName || fieldKey}`; rules[fieldKey] = [{ required: true, - message: `${useChangeTrigger ? "请选择" : "请填写"}${requirement.fieldName || fieldKey}`, + message, trigger: useChangeTrigger ? "change" : "blur" }]; }); @@ -2570,6 +2605,21 @@ onBeforeUnmount(() => { border-color: #059669 !important; } +.readonly-derived-input :deep(.el-input__wrapper) { + background-color: #f9fafb !important; + cursor: default; +} + +.readonly-derived-input :deep(.el-input__inner) { + color: #374151 !important; + -webkit-text-fill-color: #374151 !important; + font-weight: 500; +} + +.readonly-derived-input :deep(.el-input__prefix) { + color: #059669; +} + @media (max-width: 640px) { .fill-page :deep(.el-input__inner),