Files
Docker/uniapp/youjiala/src/pages/security/family-person-operation.vue
T
2026-10-19 13:36:58 +08:00

739 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* 家族成员详情(查看 / 修改):旧 WebVue views/master/FamilyPersonOperation.vue 的移动端等价物
*
* 对齐旧逻辑:
* - FindByKey(PersonId) 取 FamilyPersonData,主对象 FamilyPerson;
* - 头像上传 file-path=FamilyPersonAvatar(旧 FileUpload 的移动端等价:chooseImage 单图 +
* uploadFile 直传 DmsFile/UploadFile,FilePath 走请求头、form 字段名=原始文件名、
* filename=分片三段式物理名并同存 AvatarFilename);
* - 性别是 GenderPolicy 枚举,admin-form 无 select,用 #extra 行点开 option-picker 回写;
* - 身份证上传:#extra 里的身份证块,chooseImage 单图 → 单通道上传同源后端 DmsFile/UploadIdCardFile
* (FilePath 头=idcard 目录、form 字段 side=auto)→ 后端先落盘再 server-to-server 调同 docker 的
* OCR 服务 /api/v1/idcard/recognize,把识别结果回填进返回的 FamilyPersonDisplayData(前端不直连 OCR,避免浏览器跨域);
* 前端拿到返回后:常用名/性别/出生日期 回填各自真实列;民族/住址/身份证号/签发机关/有效期 表内无列、不落库,
* 故不显示为表单字段,而是格式化写入「简介」(introduction 列)随保存落库;
* - 只读门控:!IsCanAmend 且 Privacy!=='Y' 时只显「基本资料」卡、隐藏其余卡与修改/删除;
* - 保存按 CreatedBy 有无决定 Update / Insert。
* 旧页的新增入口在查询页即被注释,本页只承担 view/amend 两态(create 走快速建员弹层)。
*/
import type { AdminFormAction, AdminFormField, AdminFormSection } from "@/admin/admin-types";
import { onLoad } from "@dcloudio/uni-app";
import { useLocale } from "uview-pro";
import { computed, ref } from "vue";
import { buildChunkFileName, uploadFile } from "@/common/http.interceptor";
import { useAuthGuard } from "@/composables";
import { usePolicyOptions } from "@/composables/usePolicyOptions";
import { FamilyPersonData } from "@/data/auto/familyPersonData";
import { smartBack } from "@/router/routes";
import familyPersonService from "@/service/master/familyPersonService";
const { t } = useLocale();
const { getPolicyOptions, getPolicyText } = usePolicyOptions();
const data = ref<FamilyPersonData>(new FamilyPersonData());
const person = computed(() => data.value.FamilyPerson);
const readonly = ref(true);
const routeId = ref("");
const genderPickerVisible = ref(false);
const uploading = ref(false);
const idcardUploading = ref(false);
const genderItems = computed(() => getPolicyOptions("GenderPolicy"));
const genderLabel = computed(() => getPolicyText("GenderPolicy", person.value.Gender));
/** 只读门控(旧页同款):无权修改且本人未公开详细资料时,只保留基本资料卡 */
const readonlyBasicOnly = computed(
() => routeId.value !== "" && !person.value.IsCanAmend && person.value.Privacy !== "Y",
);
const heroTitle = computed(
() => `${person.value.Surname ?? ""}${person.value.GenealogyName ?? ""}`,
);
const heroChips = computed(() => {
const chips: string[] = [];
if (person.value.PersonId) {
chips.push(`${t("FamilyPersonPage.personId")}: ${person.value.PersonId}`);
}
if (person.value.TreeId) {
chips.push(`${t("FamilyPersonPage.treeId")}: ${person.value.TreeId}`);
}
return chips;
});
const sections = computed<AdminFormSection[]>(() => {
const basic: AdminFormSection = {
title: t("FamilyPersonPage.basicTitle"),
fields: [
{ label: t("FamilyPersonPage.surname"), prop: "Surname", required: true },
{ label: t("FamilyPersonPage.genealogyName"), prop: "GenealogyName", required: true },
{ label: t("FamilyPersonPage.commonName"), prop: "CommonName" },
{ label: t("FamilyPersonPage.alias"), prop: "Alias" },
{ label: t("FamilyPersonPage.zi"), prop: "Zi" },
{ label: t("FamilyPersonPage.hao"), prop: "Hao" },
{ label: t("FamilyPersonPage.birthOrder"), prop: "BirthOrder" },
],
};
if (readonlyBasicOnly.value) {
return [basic];
}
return [
basic,
{
title: t("FamilyPersonPage.familyTitle"),
fields: [
{ label: t("FamilyPersonPage.maritalStatus"), prop: "MaritalStatus" },
{ label: t("FamilyPersonPage.fatherId"), prop: "FatherId" },
{ label: t("FamilyPersonPage.motherName"), prop: "MotherName" },
{ label: t("FamilyPersonPage.school"), prop: "School" },
{ label: t("FamilyPersonPage.major"), prop: "Major" },
{ label: t("FamilyPersonPage.education"), prop: "Education" },
{ label: t("FamilyPersonPage.graduationStatus"), prop: "GraduationStatus" },
{ label: t("FamilyPersonPage.duty"), prop: "Duty" },
],
},
{
title: t("FamilyPersonPage.workTitle"),
fields: [
{ label: t("FamilyPersonPage.title"), prop: "Title" },
{
label: t("FamilyPersonPage.privacy"),
prop: "Privacy",
type: "switch",
display: model => (model.Privacy === "Y" ? t("FamilyPersonPage.yes") : t("FamilyPersonPage.no")),
},
{ label: t("FamilyPersonPage.liveStatus"), prop: "LiveStatus" },
{ label: t("FamilyPersonPage.bindUserId"), prop: "BindUserId" },
{
label: t("FamilyPersonPage.bindUser"),
prop: "IsBindUser",
type: "switch",
display: model => (model.IsBindUser === "Y" ? t("FamilyPersonPage.yes") : t("FamilyPersonPage.no")),
},
{ label: t("FamilyPersonPage.birthText"), prop: "BirthText" },
{ label: t("FamilyPersonPage.birthDate"), prop: "BirthDate", type: "date", date: true },
{ label: t("FamilyPersonPage.deathText"), prop: "DeathText" },
{ label: t("FamilyPersonPage.deathDate"), prop: "DeathDate", type: "date", date: true },
],
},
{
title: t("FamilyPersonPage.introTitle"),
fields: [
{
label: t("FamilyPersonPage.introduction"),
prop: "Introduction",
type: "textarea",
maxlength: 2000,
},
],
},
];
});
const actions = computed<AdminFormAction[]>(() => {
const list: AdminFormAction[] = [];
if (readonlyBasicOnly.value) {
return list;
}
list.push({ key: "amend", label: t("AdminPage.amendBut"), when: "view" });
// 旧页口径:仅未挂族谱(TreeId 为空)的成员允许删除
if (person.value.TreeId === "") {
list.push({
key: "delete",
label: t("AdminPage.deleteBut"),
tone: "error",
confirm: true,
when: "view",
});
}
list.push({ key: "save", label: t("AdminPage.saveBut"), when: "edit" });
list.push({ key: "cancel", label: t("AdminPage.cancelBut"), when: "edit" });
return list;
});
async function load(): Promise<void> {
const result = await familyPersonService.FindByKey(routeId.value);
if (!result?.FamilyPerson?.PersonId) {
uni.showToast({ title: t("AdminPage.notExistTip"), icon: "none" });
return;
}
data.value = result;
}
function onFieldChange(prop: string, value: unknown): void {
if (prop === "Privacy" || prop === "IsBindUser") {
person.value[prop] = value ? "Y" : "N";
return;
}
(person.value as Record<string, any>)[prop] = value;
}
function onGenderConfirm(key: string): void {
person.value.Gender = key;
genderPickerVisible.value = false;
}
/** 头像上传:单图 -> DmsFile/UploadFile(旧 FileUpload 默认通道,FilePath 头指定 FamilyPersonAvatar)。
* 后端按 {Backup}/{FilePath}/{ext}/{分片名} 落盘,FindByKey 的 AvatarUrl 也按此拼 URL,
* 故 AvatarFilename 必须存分片物理名(buildChunkFileName),不能存原始文件名 */
async function onUploadAvatar(): Promise<void> {
if (uploading.value) {
return;
}
uni.chooseImage({
count: 1,
success: async (res: any) => {
const filePath = String(res?.tempFilePaths?.[0] ?? "");
const fileName = String(res?.tempFiles?.[0]?.name || `avatar-${Date.now()}.png`);
if (!filePath) {
return;
}
try {
uploading.value = true;
const physicalName = buildChunkFileName(fileName);
await uploadFile({
url: "/DmsFile/UploadFile",
filePath,
name: fileName,
fileName: physicalName,
header: { FilePath: "FamilyPersonAvatar" },
});
person.value.AvatarFilename = physicalName;
person.value.AvatarUrl = filePath;
uni.showToast({ title: t("FamilyPersonPage.avatarUploadedTip"), icon: "none" });
}
catch (error) {
console.error(error);
uni.showToast({ title: t("FamilyPersonPage.avatarUploadFailed"), icon: "none" });
}
finally {
uploading.value = false;
}
},
});
}
function onClearAvatar(): void {
// H5 本地预览是 blob: URL,清掉引用防泄漏;其余端是本地临时路径,无需回收
if (person.value.AvatarUrl.startsWith("blob:") && typeof URL !== "undefined") {
URL.revokeObjectURL(person.value.AvatarUrl);
}
person.value.AvatarFilename = "";
person.value.AvatarUrl = "";
}
/** 身份证 OCR 性别文本("男"/"女"/"male"/"female")反查 GenderPolicy 存储键:
* 先按 key、再按 label 精确,最后按含「男/女」模糊匹配,全落空返回空串(键由后端语言包决定,不写死) */
function matchGenderKey(text: string): string {
const value = text.trim();
if (!value) {
return "";
}
const options = getPolicyOptions("GenderPolicy");
const exact = options.find(o => o.key === value || o.label === value);
if (exact) {
return exact.key;
}
const lower = value.toLowerCase();
const wantMale = value.includes("男") || lower.includes("male");
const wantFemale = value.includes("女") || lower.includes("female");
if (!wantMale && !wantFemale) {
return "";
}
const hit = options.find((o) => {
const label = `${o.key} ${o.label}`.toLowerCase();
return (wantMale && (o.label.includes("男") || label.includes("male")))
|| (wantFemale && (o.label.includes("女") || label.includes("female")));
});
return hit?.key ?? "";
}
/** 出生日期归一为 yyyy-MM-dd(OCR 常给 "19791111",也兼容带分隔符);不足 8 位数字返回空串 */
function formatBirthToYmd(raw: string): string {
const digits = String(raw ?? "").replace(/\D/g, "");
if (digits.length < 8) {
return "";
}
return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
}
/** 把后端 UploadIdCardFile 返回的识别信息(FamilyPersonDisplayData)回填到表单:
* 常用名/性别/出生日期有对应 family_person 列,直接回填(性别回传原始文本按 GenderPolicy 反查键,出生归一 yyyy-MM-dd);
* 民族/住址/身份证号/签发机关/有效期限 表里没有列、存不进库,故不作为表单字段显示,
* 而是格式化追加进「简介」(introduction 是真实列,能落库)。字段值兼容 PascalCase 与 camelCase;有任一写入返回 true */
function applyIdCardOcr(info: any): boolean {
if (!info || typeof info !== "object") {
return false;
}
const pick = (pascal: string, camel: string): unknown => info[pascal] ?? info[camel];
let filled = 0;
const name = String(pick("CommonName", "commonName") ?? "").trim();
if (name) {
person.value.CommonName = name;
filled += 1;
}
const genderKey = matchGenderKey(String(pick("Gender", "gender") ?? ""));
if (genderKey) {
person.value.Gender = genderKey;
filled += 1;
}
const ymd = formatBirthToYmd(String(pick("BirthDate", "birthDate") ?? ""));
if (ymd) {
(person.value as Record<string, any>).BirthDate = ymd;
filled += 1;
}
if (upsertIdCardIntro(info)) {
filled += 1;
}
return filled > 0;
}
/** 把无列的身份信息(民族/住址/身份证号/签发机关/有效期限)格式化成一个「身份证识别信息」块写入简介:
* 该块恒追加在简介末尾,重新识别时先截掉旧块再追加,避免重复堆叠;无任何可写项返回 false(不动简介) */
function upsertIdCardIntro(info: any): boolean {
const pick = (pascal: string, camel: string): unknown => info[pascal] ?? info[camel];
const labelMap: Array<[string, string, string]> = [
["nation", "Nation", "nation"],
["address", "Address", "address"],
["idNumber", "IdNumber", "idNumber"],
["issuer", "Issuer", "issuer"],
["validity", "Validity", "validity"],
];
const lines: string[] = [];
for (const [labelKey, pascal, camel] of labelMap) {
const val = String(pick(pascal, camel) ?? "").trim();
if (val) {
lines.push(`${t(`FamilyPersonPage.${labelKey}`)}: ${val}`);
}
}
if (!lines.length) {
return false;
}
const title = t("FamilyPersonPage.idcardIntroTitle");
const block = [title, ...lines].join("\n");
let intro = String(person.value.Introduction ?? "");
const idx = intro.indexOf(title);
if (idx >= 0) {
// 旧识别块恒在末尾:截掉旧块(含其后内容)后再追加新块,避免重复
intro = intro.slice(0, idx).replace(/\s+$/, "");
}
person.value.Introduction = intro ? `${intro}\n\n${block}` : block;
return true;
}
/** 上传身份证:单图 → 同源后端 DmsFile/UploadIdCardFile(后端落盘 idcard 目录 + server-to-server 调 OCR)
* → 用返回的识别信息回填字段;前端不直连 OCR,避免浏览器跨域 */
async function onUploadIdCard(): Promise<void> {
if (idcardUploading.value) {
return;
}
uni.chooseImage({
count: 1,
success: async (res: any) => {
const filePath = String(res?.tempFilePaths?.[0] ?? "");
const fileName = String(res?.tempFiles?.[0]?.name || `idcard-${Date.now()}.jpg`);
if (!filePath) {
return;
}
try {
idcardUploading.value = true;
// 单通道:本地预览先填,物理名(分片三段式)与头像 AvatarFilename 同口径
const physicalName = buildChunkFileName(fileName);
person.value.IdCardFilename = physicalName;
person.value.IdCardUrl = filePath;
// 上传到后端:FilePath 头=idcard 目录,side=auto;后端落盘后调 OCR 并把识别结果放进返回体
const result = await uploadFile({
url: "/DmsFile/UploadIdCardFile",
filePath,
name: fileName,
fileName: physicalName,
header: { FilePath: "idcard" },
formData: { side: "auto" },
});
// 后端返回 FamilyPersonDisplayData,业务响应封装体在 Data/data;容错取内层
const recognized = result?.Data ?? result?.data ?? result;
if (applyIdCardOcr(recognized)) {
uni.showToast({ title: t("FamilyPersonPage.idcardUploadedTip"), icon: "none" });
}
else {
uni.showToast({ title: t("FamilyPersonPage.idcardNoResult"), icon: "none" });
}
}
catch (error) {
console.error(error);
uni.showToast({ title: t("FamilyPersonPage.idcardUploadFailed"), icon: "none" });
}
finally {
idcardUploading.value = false;
}
},
});
}
function onClearIdCard(): void {
if (person.value.IdCardUrl.startsWith("blob:") && typeof URL !== "undefined") {
URL.revokeObjectURL(person.value.IdCardUrl);
}
person.value.IdCardFilename = "";
person.value.IdCardUrl = "";
}
async function onSave(): Promise<void> {
if (!String(person.value.Surname ?? "").trim()) {
uni.showToast({ title: t("FamilyPersonPage.surnameRequired"), icon: "none" });
return;
}
if (!String(person.value.GenealogyName ?? "").trim()) {
uni.showToast({ title: t("FamilyPersonPage.genealogyNameRequired"), icon: "none" });
return;
}
const isUpdate = String(person.value.CreatedBy ?? "").trim() !== "";
const result = await (isUpdate
? familyPersonService.Update(data.value)
: familyPersonService.Insert(data.value));
if (result?.FamilyPerson?.PersonId) {
data.value = result;
routeId.value = result.FamilyPerson.PersonId;
}
readonly.value = true;
uni.showToast({ title: t("AdminPage.savedTip"), icon: "none" });
}
async function onDelete(): Promise<void> {
await familyPersonService.Delete(routeId.value);
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
setTimeout(() => smartBack("mineTab"), 600);
}
function onAction(action: AdminFormAction): void {
switch (action.key) {
case "amend":
readonly.value = false;
break;
case "save":
onSave();
break;
case "delete":
onDelete();
break;
case "cancel":
if (!routeId.value) {
smartBack("mineTab");
return;
}
readonly.value = true;
load();
break;
default:
break;
}
}
function onBack(): void {
smartBack("mineTab");
}
onLoad((options) => {
routeId.value = String(options?.id ?? "");
const mode = String(options?.mode ?? "view");
readonly.value = mode !== "create" && mode !== "amend";
});
let booted = false;
useAuthGuard(() => {
if (booted) {
return;
}
booted = true;
if (routeId.value) {
load();
}
});
</script>
<template>
<admin-form :nav-title="t('FamilyPersonPage.detailTitle')" :show-nav-back="true" :hero-title="heroTitle"
:hero-chips="heroChips" :sections="sections" :model="person" :readonly="readonly" :actions="actions"
@back="onBack" @action="onAction" @field-change="onFieldChange">
<!-- 旧系统同款:头像就在 hero 卡左侧,显示与上传同一处;编辑态右侧出「清除头像」 -->
<template #hero-side="{ readonly: ro }">
<view class="hero-avatar" @tap="!ro && onUploadAvatar()">
<image v-if="person.AvatarUrl" class="hero-avatar__img" :src="person.AvatarUrl" mode="aspectFill" />
<u-icon v-else name="account" size="56" color="#ffffff" />
<view v-if="!ro" class="hero-avatar__mask">
<text v-if="uploading" class="hero-avatar__mask-text">
{{ t("FamilyPersonPage.uploadingTip") }}
</text>
<u-icon v-else name="camera" size="28" color="#ffffff" />
</view>
</view>
</template>
<template #hero-end="{ readonly: ro }">
<view v-if="!ro && (person.AvatarUrl || person.AvatarFilename)" class="hero-clear" @tap="onClearAvatar">
<text class="hero-clear-text">
{{ t("FamilyPersonPage.clearAvatar") }}
</text>
</view>
</template>
<template #extra="{ readonly: ro }">
<!-- 只读门控提示:无修改权限且对方未公开详细资料 -->
<view v-if="readonlyBasicOnly" class="ro-tip">
<text>
{{ t("FamilyPersonPage.readonlyTip") }}
</text>
</view>
<template v-else>
<!-- 性别(admin-form 无 select,枚举行在这里点开的 option-picker 里回写) -->
<view class="group">
<view class="group-card">
<view class="enum-row" :class="{ 'enum-row--tap': !ro }"
@tap="!ro && (genderPickerVisible = true)">
<text class="enum-label">
{{ t("FamilyPersonPage.gender") }}
</text>
<view class="enum-value-box">
<text class="enum-value" :class="{ 'enum-value-empty': !genderLabel }">
{{ genderLabel || t("FamilyPersonPage.genderPlaceholder") }}
</text>
<u-icon v-if="!ro" name="arrow-right" size="24" color="#c4c8cf" />
</view>
</view>
</view>
</view>
<!-- 身份证上传块(编辑态可传:后端落盘 idcard 目录并 OCR 后回填身份字段) -->
<view class="group">
<view class="group-card idcard-card">
<view class="idcard-head">
<text class="idcard-title">{{ t("FamilyPersonPage.idcardTitle") }}</text>
<text v-if="!ro && (person.IdCardUrl || person.IdCardFilename)" class="idcard-clear"
@tap="onClearIdCard">{{ t("FamilyPersonPage.idcardClear") }}</text>
</view>
<view class="idcard-thumb" :class="{ 'idcard-thumb--tap': !ro }" @tap="!ro && onUploadIdCard()">
<image v-if="person.IdCardUrl" class="idcard-thumb__img" :src="person.IdCardUrl"
mode="aspectFill" />
<view v-else class="idcard-thumb__empty">
<u-icon name="camera" size="40" color="#c4c8cf" />
<text class="idcard-thumb__empty-text">{{ t("FamilyPersonPage.idcardUpload") }}</text>
</view>
<view v-if="!ro" class="idcard-thumb__mask">
<text v-if="idcardUploading" class="idcard-thumb__mask-text">
{{ t("FamilyPersonPage.uploadingTip") }}
</text>
<u-icon v-else-if="person.IdCardUrl" name="camera" size="30" color="#ffffff" />
</view>
</view>
<text class="idcard-hint">{{ t("FamilyPersonPage.idcardHint") }}</text>
</view>
</view>
</template>
<option-picker :visible="genderPickerVisible" :title="t('FamilyPersonPage.gender')" :items="genderItems"
:empty-text="t('FamilyPersonPage.genderEmpty')" :confirm-text="t('AdminPage.confirmBut')"
@close="genderPickerVisible = false" @confirm="onGenderConfirm" />
</template>
</admin-form>
</template>
<style lang="scss" scoped>
.ro-tip {
padding: 28rpx;
margin-bottom: 28rpx;
font-size: 24rpx;
color: $u-tips-color;
background-color: #ffffff;
border-radius: 20rpx;
}
.group {
margin-bottom: 28rpx;
}
.group-head {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 12rpx 12rpx;
}
.group-title {
font-size: 24rpx;
color: $u-tips-color;
}
.group-card {
padding: 0 28rpx;
overflow: hidden;
background-color: #ffffff;
border-radius: 20rpx;
}
/* hero 左侧头像(旧系统 ft-hero-avatar 同款):显示与上传同一处,编辑态覆相机遮罩 */
.hero-avatar {
position: relative;
display: flex;
flex: none;
align-items: center;
justify-content: center;
width: 120rpx;
height: 120rpx;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.18);
border: 4rpx solid rgba(255, 255, 255, 0.55);
border-radius: 50%;
}
.hero-avatar__img {
width: 100%;
height: 100%;
}
.hero-avatar__mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.35);
}
.hero-avatar__mask-text {
font-size: 20rpx;
color: #ffffff;
}
/* hero 右侧「清除头像」白胶囊(旧系统同款位) */
.hero-clear {
flex: none;
padding: 10rpx 24rpx;
background-color: rgba(255, 255, 255, 0.22);
border-radius: 999rpx;
}
.hero-clear-text {
font-size: 24rpx;
color: #ffffff;
}
.enum-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
min-height: 92rpx;
}
.enum-label {
font-size: 28rpx;
color: $u-main-color;
}
.enum-value-box {
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
}
.enum-value {
font-size: 26rpx;
color: $u-content-color;
}
.enum-value-empty {
color: $u-tips-color;
}
/* 身份证上传块(编辑态可点,识别后回填身份字段) */
.idcard-card {
padding: 28rpx;
}
.idcard-head {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.idcard-title {
font-size: 28rpx;
color: $u-main-color;
}
.idcard-clear {
font-size: 24rpx;
color: $u-tips-color;
}
.idcard-thumb {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 320rpx;
overflow: hidden;
background-color: #f5f6f8;
border: 2rpx dashed #d6d9de;
border-radius: 16rpx;
}
.idcard-thumb--tap:active {
opacity: 0.7;
}
.idcard-thumb__img {
width: 100%;
height: 100%;
}
.idcard-thumb__empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.idcard-thumb__empty-text {
font-size: 26rpx;
color: $u-tips-color;
}
.idcard-thumb__mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.35);
}
.idcard-thumb__mask-text {
font-size: 24rpx;
color: #ffffff;
}
.idcard-hint {
display: block;
margin-top: 16rpx;
font-size: 22rpx;
line-height: 1.5;
color: $u-tips-color;
}
</style>