diff --git a/apps/web-antd/src/api/request.ts b/apps/web-antd/src/api/request.ts
index 5edad1c5..1e1071bb 100644
--- a/apps/web-antd/src/api/request.ts
+++ b/apps/web-antd/src/api/request.ts
@@ -16,6 +16,12 @@ import { message } from 'ant-design-vue';
// eslint-disable-next-line n/no-extraneous-import
import { Base64 } from 'js-base64';
+import {
+ ENCRYPT_FIELDS,
+ isSensitiveField,
+ maskSensitiveField,
+ TM_TEXT_SUFFIX,
+} from '#/constants/sensitive-fields';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
@@ -23,33 +29,6 @@ import {downloadByData} from "#/util/tool";
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
-const arr = new Set([
- 'account',
- 'accept_tel',
- 'doctor',
- 'express_mobile',
- 'id_card',
- 'idcard',
- 'mobile',
- 'password',
- 'patient_mobile',
- 'phone',
-]);
-
-// 敏感数据
-const sensitiveData = new Set([
- 'account',
- 'accept_tel',
- 'doctor',
- 'express_mobile',
- 'id_card',
- 'idcard',
- 'mobile',
- 'password',
- 'patient_mobile',
- 'phone',
-]);
-
function createRequestClient(baseURL: string) {
const client = new RequestClient({
baseURL,
@@ -189,61 +168,34 @@ export const baseRequestClient = new RequestClient({ baseURL: apiURL });
*/
function getRes(obj: any, isDecode = true) {
try {
+ if (obj == null || typeof obj !== 'object') {
+ return obj;
+ }
for (const key in obj) {
+ // 跳过脱敏展示字段,避免重复处理
+ if (key.endsWith(TM_TEXT_SUFFIX)) {
+ continue;
+ }
if (Array.isArray(obj[key])) {
- obj[key] = getRes(obj[key]);
- } else if (typeof obj[key] === 'object') {
- obj[key] = getRes(obj[key]);
- } else {
- // 判断obj[key]是否在arr中
- if (arr.has(key)) {
- let aseFile = '';
- aseFile = isDecode
- ? customBase64Decode(obj[key], key)
- : customBase64Encode(obj[key]);
- const isGarbled =
- // eslint-disable-next-line no-control-regex
- /[\u0000-\u0008\v\f\u000E-\u001F\u007F-\u00FF\u{E000}-\u{F8FF}]/u.test(
- aseFile,
- );
- if (isGarbled) {
- aseFile = obj[key];
- }
- if (isDecode && sensitiveData.has(key)) {
- // 数据脱敏,自动匹配姓名、手机号、身份证
- // switch (key) {
- // case 'accept_name':
- // case 'express_name':
- // case 'express_region':
- // case 'patient': {
- // // 保留前两个字符,其余用星号代替
- // aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*');
- // break;
- // }
- // case 'express_mobile':
- // case 'mobile':
- // case 'phone': {
- // // 保留前三位和后四位,中间用星号代替
- // aseFile =
- // aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') +
- // aseFile.slice(-4);
- // break;
- // }
- // case 'id_card':
- // case 'idcard': {
- // // 保留前六位和后四位,中间用星号代替
- // aseFile =
- // aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') +
- // aseFile.slice(-4);
- // break;
- // }
- // default: {
- // // 默认情况下,全部用星号代替
- // break;
- // }
- // }
- }
- obj[key] = aseFile;
+ obj[key] = getRes(obj[key], isDecode);
+ } else if (obj[key] !== null && typeof obj[key] === 'object') {
+ obj[key] = getRes(obj[key], isDecode);
+ } else if (ENCRYPT_FIELDS.has(key)) {
+ let plainValue = isDecode
+ ? customBase64Decode(obj[key], key)
+ : customBase64Encode(obj[key]);
+ const isGarbled =
+ // eslint-disable-next-line no-control-regex
+ /[\u0000-\u0008\v\f\u000E-\u001F\u007F-\u00FF\u{E000}-\u{F8FF}]/u.test(
+ plainValue,
+ );
+ if (isGarbled) {
+ plainValue = obj[key];
+ }
+ obj[key] = plainValue;
+ // 解密后写入脱敏展示字段,明文保留在原字段
+ if (isDecode && isSensitiveField(key)) {
+ obj[key + TM_TEXT_SUFFIX] = maskSensitiveField(key, plainValue);
}
}
}
diff --git a/apps/web-antd/src/components/doctor-transfer-float/DoctorTransferFloat.vue b/apps/web-antd/src/components/doctor-transfer-float/DoctorTransferFloat.vue
index c5688cee..1392782f 100644
--- a/apps/web-antd/src/components/doctor-transfer-float/DoctorTransferFloat.vue
+++ b/apps/web-antd/src/components/doctor-transfer-float/DoctorTransferFloat.vue
@@ -33,6 +33,7 @@ import {
} from 'ant-design-vue';
import FloatFab from '#/components/float-fab/FloatFab.vue';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import {
DEFAULT_SUB_WINDOW_Z_INDEX,
getSubWindowPopupZIndex,
@@ -462,7 +463,10 @@ defineExpose({ loadList });
收货人
- {{ item.patient_name }} {{ item.patient_mobile }}
+
+ {{ item.patient_name }}
+
+
地址
diff --git a/apps/web-antd/src/components/form/components/doctor-picker.vue b/apps/web-antd/src/components/form/components/doctor-picker.vue
index b0e050b5..94ba5b68 100644
--- a/apps/web-antd/src/components/form/components/doctor-picker.vue
+++ b/apps/web-antd/src/components/form/components/doctor-picker.vue
@@ -5,6 +5,7 @@ import { useVModel } from '@vueuse/core';
import { Avatar, Empty, Input, Popover, Spin } from 'ant-design-vue';
import { getDoctorOptionApi } from '#/views/system/store/api';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
defineOptions({
@@ -156,7 +157,9 @@ watch(
{{ item.name || '-' }}
-
{{ item.mobile }}
+
+
+
{{ item.depart_name }}
@@ -174,7 +177,13 @@ watch(
{{ selectedOption.name }}
- {{ selectedOption.mobile || `su_id: ${selectedOption.id}` }}
+
+ su_id: {{ selectedOption.id }}
{
{{ item.nick_name || '-' }}
{{ item.code }}
- {{ item.phone }}
+
+
+
{{ item.role_name || '业务员' }}
diff --git a/apps/web-antd/src/components/form/components/store-picker.vue b/apps/web-antd/src/components/form/components/store-picker.vue
index 34a3dbe1..a413ec49 100644
--- a/apps/web-antd/src/components/form/components/store-picker.vue
+++ b/apps/web-antd/src/components/form/components/store-picker.vue
@@ -5,6 +5,7 @@ import { useVModel } from '@vueuse/core';
import { Avatar, Empty, Input, Popover, Spin } from 'ant-design-vue';
import { getStoreOption } from '#/views/system/store/api';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
defineOptions({
@@ -133,7 +134,9 @@ onMounted(() => {
{{ item.name || '-' }}
- {{ item.mobile }}
+
+
+
ID: {{ item.id }}
@@ -149,7 +152,13 @@ onMounted(() => {
{{ selectedOption.name }}
- {{ selectedOption.mobile || `ID: ${selectedOption.id}` }}
+
+ ID: {{ selectedOption.id }}
{{ detail.code || '-' }}
- {{ detail.phone || '-' }}
+
+
+
{{ detail.clinic_input_count ?? 0 }} 家
diff --git a/apps/web-antd/src/components/promoter/PromoterInfoCard.vue b/apps/web-antd/src/components/promoter/PromoterInfoCard.vue
index 207ed10f..96558145 100644
--- a/apps/web-antd/src/components/promoter/PromoterInfoCard.vue
+++ b/apps/web-antd/src/components/promoter/PromoterInfoCard.vue
@@ -6,6 +6,7 @@ import { useVbenModal } from '@vben/common-ui';
import { Avatar } from 'ant-design-vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import PromoterDetailModal from './PromoterDetailModal.vue';
@@ -96,7 +97,7 @@ function onClear(e: Event) {
业务码:{{ admin.code }}
- {{ admin.phone }}
+
+import type { SensitiveFieldSource } from '#/constants/sensitive-fields';
+
+import { computed, ref } from 'vue';
+
+import { useClipboard } from '@vueuse/core';
+import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons-vue';
+import { message } from 'ant-design-vue';
+
+import {
+ getSensitiveMaskText,
+ getSensitivePlainText,
+} from '#/constants/sensitive-fields';
+
+const props = withDefaults(
+ defineProps<{
+ /** 数据对象 */
+ record?: Record | null;
+ /** 字段名 */
+ field: string;
+ /** 主字段为空时的备选来源 */
+ fallback?: SensitiveFieldSource;
+ /** 空值占位 */
+ emptyText?: string;
+ /** 是否显示眼睛切换 */
+ showEye?: boolean;
+ /** 点击文本是否复制明文 */
+ copyable?: boolean;
+ }>(),
+ {
+ record: null,
+ emptyText: '—',
+ showEye: true,
+ copyable: true,
+ },
+);
+
+const revealed = ref(false);
+const { copy } = useClipboard({ legacy: true });
+
+/** 明文值,复制与展示切换时使用 */
+const plainText = computed(() =>
+ getSensitivePlainText(props.record, props.field, props.fallback),
+);
+
+/** 脱敏展示值 */
+const maskText = computed(() =>
+ getSensitiveMaskText(props.record, props.field, props.fallback),
+);
+
+/** 当前界面展示文本 */
+const displayText = computed(() => {
+ if (!plainText.value) {
+ return props.emptyText;
+ }
+ return revealed.value ? plainText.value : maskText.value;
+});
+
+/** 是否有有效值(非空占位) */
+const hasValue = computed(() => plainText.value !== '');
+
+/**
+ * 点击文本复制明文(脱敏前的原始值)
+ */
+async function handleCopyText() {
+ if (!props.copyable || !hasValue.value) {
+ return;
+ }
+ try {
+ await copy(plainText.value);
+ message.success('复制成功');
+ } catch {
+ message.error('复制失败');
+ }
+}
+
+/**
+ * 切换明文 / 脱敏展示
+ */
+function toggleReveal() {
+ if (!hasValue.value) {
+ return;
+ }
+ revealed.value = !revealed.value;
+}
+
+
+
+
+
+ {{ displayText }}
+
+
+
+
+
+
+
diff --git a/apps/web-antd/src/constants/sensitive-fields.ts b/apps/web-antd/src/constants/sensitive-fields.ts
new file mode 100644
index 00000000..c849d213
--- /dev/null
+++ b/apps/web-antd/src/constants/sensitive-fields.ts
@@ -0,0 +1,133 @@
+/**
+ * 敏感字段集中定义:拦截器脱敏与 SensitiveText 组件共用
+ */
+
+/** 脱敏展示后缀,明文保留在原字段 */
+export const TM_TEXT_SUFFIX = '_tm_text';
+
+/** 需要加解密的字段(与 request 拦截器 arr 一致) */
+export const ENCRYPT_FIELDS = new Set([
+ 'account',
+ 'accept_tel',
+ 'doctor',
+ 'express_mobile',
+ 'id_card',
+ 'idcard',
+ 'mobile',
+ 'password',
+ 'patient_mobile',
+ 'phone',
+]);
+
+/** 需要生成 _tm_text 并默认脱敏展示的字段(encrypt_only) */
+export const MASK_DISPLAY_FIELDS = ENCRYPT_FIELDS;
+
+const PHONE_FIELDS = new Set([
+ 'mobile',
+ 'phone',
+ 'express_mobile',
+ 'patient_mobile',
+ 'accept_tel',
+]);
+
+const ID_CARD_FIELDS = new Set(['id_card', 'idcard']);
+
+export interface SensitiveFieldSource {
+ record?: Record | null;
+ field: string;
+}
+
+/**
+ * 判断是否为敏感字段
+ */
+export function isSensitiveField(field: string): boolean {
+ return MASK_DISPLAY_FIELDS.has(field);
+}
+
+/**
+ * 从 record 读取明文,支持 fallback 链
+ */
+export function getSensitivePlainText(
+ record: Record | null | undefined,
+ field: string,
+ fallback?: SensitiveFieldSource,
+): string {
+ const primary = readFieldValue(record, field);
+ if (primary !== '') {
+ return primary;
+ }
+ if (fallback?.record) {
+ return readFieldValue(fallback.record, fallback.field);
+ }
+ return '';
+}
+
+/**
+ * 读取脱敏文本:优先 _tm_text,否则对明文现场脱敏
+ */
+export function getSensitiveMaskText(
+ record: Record | null | undefined,
+ field: string,
+ fallback?: SensitiveFieldSource,
+): string {
+ const tmKey = `${field}${TM_TEXT_SUFFIX}`;
+ const tmFromPrimary = readFieldValue(record, tmKey);
+ if (tmFromPrimary !== '') {
+ return tmFromPrimary;
+ }
+ if (fallback?.record) {
+ const tmFromFallback = readFieldValue(fallback.record, tmKey);
+ if (tmFromFallback !== '') {
+ return tmFromFallback;
+ }
+ }
+ const plain = getSensitivePlainText(record, field, fallback);
+ if (plain === '') {
+ return '';
+ }
+ return maskSensitiveField(field, plain);
+}
+
+/**
+ * 按字段类型生成脱敏文本
+ */
+export function maskSensitiveField(field: string, plainText: unknown): string {
+ if (plainText == null || plainText === '') {
+ return '';
+ }
+ const str = String(plainText);
+ if (field === 'password') {
+ return '*'.repeat(Math.max(str.length, 6));
+ }
+ if (PHONE_FIELDS.has(field)) {
+ if (str.length <= 7) {
+ return str;
+ }
+ return str.slice(0, 3) + '*'.repeat(str.length - 7) + str.slice(-4);
+ }
+ if (ID_CARD_FIELDS.has(field)) {
+ if (str.length <= 10) {
+ return str;
+ }
+ return str.slice(0, 6) + '*'.repeat(str.length - 10) + str.slice(-4);
+ }
+ // account / doctor 等通用兜底:保留前 2 + 后 2
+ if (str.length <= 4) {
+ return str;
+ }
+ return str.slice(0, 2) + '*'.repeat(str.length - 4) + str.slice(-2);
+}
+
+function readFieldValue(
+ record: Record | null | undefined,
+ field: string,
+): string {
+ if (!record) {
+ return '';
+ }
+ const value = record[field];
+ if (value == null || value === '') {
+ return '';
+ }
+ return String(value);
+}
diff --git a/apps/web-antd/src/views/business/clinic-salesperson/earnings.vue b/apps/web-antd/src/views/business/clinic-salesperson/earnings.vue
index 83f1cf00..1b032c0b 100644
--- a/apps/web-antd/src/views/business/clinic-salesperson/earnings.vue
+++ b/apps/web-antd/src/views/business/clinic-salesperson/earnings.vue
@@ -8,6 +8,7 @@ import { Card, Col, Image, Row, Spin } from 'ant-design-vue';
import { getSelfMyInfoApi } from '#/views/business/clinic-salesperson/api';
import QrCodePreview from '#/views/business/store/settings/components/SalespersonQrCodePreview.vue';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
const [QrCodePreviewModal, QrCodePreviewApi] = useVbenModal({
connectedComponent: QrCodePreview,
});
@@ -37,7 +38,7 @@ function openQrCode(item: any) {
姓名:{{ info.salesperson?.nick_name }}
- 手机:{{ info.salesperson?.phone }}
+ 手机:
所属诊所:{{ info.store?.name }}
推广二维码
diff --git a/apps/web-antd/src/views/business/clinic-salesperson/index.vue b/apps/web-antd/src/views/business/clinic-salesperson/index.vue
index 27641c1a..8b54bbf5 100644
--- a/apps/web-antd/src/views/business/clinic-salesperson/index.vue
+++ b/apps/web-antd/src/views/business/clinic-salesperson/index.vue
@@ -18,6 +18,7 @@ import {
getPlatformSalespersonListApi,
openAdminAccountApi,
} from '#/views/business/store/settings/api';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import QrCodePreview from '#/views/business/store/settings/components/SalespersonQrCodePreview.vue';
import SalespersonCommissionDrawer from '#/views/system/store/components/SalespersonCommissionDrawer.vue';
import SalespersonCreateModal from '#/views/business/store/settings/components/SalespersonCreateModal.vue';
@@ -192,7 +193,7 @@ function tcmBaseLabel(type: number) {
-
{{ item.phone }}
+
邀请人:{{ item.parent_nick_name }}
diff --git a/apps/web-antd/src/views/business/order/product-order/components/detail.vue b/apps/web-antd/src/views/business/order/product-order/components/detail.vue
index 77232787..888a65cd 100644
--- a/apps/web-antd/src/views/business/order/product-order/components/detail.vue
+++ b/apps/web-antd/src/views/business/order/product-order/components/detail.vue
@@ -13,6 +13,7 @@ import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
import { formatPriceDiscountLabel, normalizeQuickOptions } from '#/utils/pricePercentAdjust';
import { expressDetailByOrderId, getOrderInfo } from '../api';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
defineOptions({
name: 'DetailModal',
@@ -310,7 +311,11 @@ function prescriptionStatusColor() {
- {{ data.address?.mobile || data.patient_mobile }}
+
{{
diff --git a/apps/web-antd/src/views/business/order/product-order/index.vue b/apps/web-antd/src/views/business/order/product-order/index.vue
index 1d536a7c..9d3f3d14 100644
--- a/apps/web-antd/src/views/business/order/product-order/index.vue
+++ b/apps/web-antd/src/views/business/order/product-order/index.vue
@@ -18,6 +18,7 @@ import { Button, Image, message, Modal as AntdModal, Popconfirm, Popover, Space,
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import {
cancelOrderApi,
getOrderInfo,
@@ -576,7 +577,11 @@ const openOrderAmountVerify = () => {
}}
- {{ row.address?.mobile || row.patient_mobile || '—' }}
+
{{ formatExpressAddress(row) }}
diff --git a/apps/web-antd/src/views/business/salesperson-transfer-prescription/components/DetailModal.vue b/apps/web-antd/src/views/business/salesperson-transfer-prescription/components/DetailModal.vue
index 96f4fc88..5a227445 100644
--- a/apps/web-antd/src/views/business/salesperson-transfer-prescription/components/DetailModal.vue
+++ b/apps/web-antd/src/views/business/salesperson-transfer-prescription/components/DetailModal.vue
@@ -4,6 +4,7 @@ import { computed, ref } from 'vue';
import { Image, Modal, Table, Tag } from 'ant-design-vue';
import { getSalespersonTransferDetailApi } from '../api';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
const visible = ref(false);
const loading = ref(false);
@@ -92,7 +93,10 @@ defineExpose({ open });
{{ detail.status_text || '—' }}
{{ detail.transfer_mode_text || '—' }}
- 患者:{{ detail.patient_name }} {{ detail.patient_mobile }}
+
+ 患者:{{ detail.patient_name }}
+
+
地址:{{ detail.patient_address || '—' }}
备注:{{ detail.remark }}
diff --git a/apps/web-antd/src/views/business/salesperson-transfer-prescription/index.vue b/apps/web-antd/src/views/business/salesperson-transfer-prescription/index.vue
index 62b107d8..632453cb 100644
--- a/apps/web-antd/src/views/business/salesperson-transfer-prescription/index.vue
+++ b/apps/web-antd/src/views/business/salesperson-transfer-prescription/index.vue
@@ -6,6 +6,7 @@ import { Page } from '@vben/common-ui';
import { Button, Empty, Spin, Table, Tag, Tooltip } from 'ant-design-vue';
import DetailModal from './components/DetailModal.vue';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { getSalespersonTransferListApi } from './api';
const loading = ref(true);
@@ -32,7 +33,11 @@ const columns = [
title: '患者',
key: 'patient',
width: 140,
- customRender: ({ record }: any) => `${record.patient_name || ''} ${record.patient_mobile || ''}`,
+ customRender: ({ record }: any) =>
+ h('span', { class: 'inline-flex items-center gap-1 flex-wrap' }, [
+ record.patient_name || '',
+ h(SensitiveText, { record, field: 'patient_mobile', showEye: false }),
+ ]),
},
{
title: '方式',
diff --git a/apps/web-antd/src/views/business/store/settings/components/SalespersonCard.vue b/apps/web-antd/src/views/business/store/settings/components/SalespersonCard.vue
index 8fd317a7..9b9707ce 100644
--- a/apps/web-antd/src/views/business/store/settings/components/SalespersonCard.vue
+++ b/apps/web-antd/src/views/business/store/settings/components/SalespersonCard.vue
@@ -14,6 +14,7 @@ import SalespersonCommissionDrawer from '#/views/business/store/settings/compone
import SalespersonCreateModal from '#/views/business/store/settings/components/SalespersonCreateModal.vue';
import SalespersonUserBindDrawer from '#/views/business/store/settings/components/SalespersonUserBindDrawer.vue';
import SalespersonInvitedDrawer from '#/views/business/store/settings/components/SalespersonInvitedDrawer.vue';
+import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
const listData = ref<{ total: number; items: any[] }>({
total: 0,
@@ -148,7 +149,7 @@ function tcmBaseLabel(type: number) {
-
{{ item.phone }}
+
邀请人:{{ item.parent_nick_name }}
diff --git a/apps/web-antd/src/views/business/store/settings/components/SalespersonInvitedDrawer.vue b/apps/web-antd/src/views/business/store/settings/components/SalespersonInvitedDrawer.vue
index 30bf08e3..eddf0e5a 100644
--- a/apps/web-antd/src/views/business/store/settings/components/SalespersonInvitedDrawer.vue
+++ b/apps/web-antd/src/views/business/store/settings/components/SalespersonInvitedDrawer.vue
@@ -1,8 +1,9 @@