1. 特色方功能迭代(固定价格),固定价格目前统一使用药品id(-102),后期会考虑新增特色方的时候自动添加一个特色方药品
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
2. 退款的时候可以退回分成
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
</div>
|
||||
<div class="card-row">
|
||||
<span class="label">收货人</span>
|
||||
<span class="value">{{ item.patient_name }} {{ item.patient_mobile }}</span>
|
||||
<span class="value">
|
||||
{{ item.patient_name }}
|
||||
<SensitiveText :record="item" field="patient_mobile" :show-eye="false" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.patient_address" class="card-row">
|
||||
<span class="label">地址</span>
|
||||
|
||||
@@ -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(
|
||||
<div class="doctor-card-name" :title="item.name">
|
||||
{{ item.name || '-' }}
|
||||
</div>
|
||||
<div v-if="item.mobile" class="doctor-card-sub">{{ item.mobile }}</div>
|
||||
<div v-if="item.mobile" class="doctor-card-sub">
|
||||
<SensitiveText :record="item" field="mobile" :show-eye="false" />
|
||||
</div>
|
||||
<div v-if="item.depart_name" class="doctor-card-sub">
|
||||
{{ item.depart_name }}
|
||||
</div>
|
||||
@@ -174,7 +177,13 @@ watch(
|
||||
<div class="doctor-selected-info">
|
||||
<div class="doctor-selected-name">{{ selectedOption.name }}</div>
|
||||
<div class="doctor-selected-sub">
|
||||
{{ selectedOption.mobile || `su_id: ${selectedOption.id}` }}
|
||||
<SensitiveText
|
||||
v-if="selectedOption.mobile"
|
||||
:record="selectedOption"
|
||||
field="mobile"
|
||||
:show-eye="false"
|
||||
/>
|
||||
<span v-else>su_id: {{ selectedOption.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
|
||||
@@ -145,7 +145,9 @@ onMounted(() => {
|
||||
{{ item.nick_name || '-' }}
|
||||
</div>
|
||||
<div class="promoter-card-sub">{{ item.code }}</div>
|
||||
<div v-if="item.phone" class="promoter-card-phone">{{ item.phone }}</div>
|
||||
<div v-if="item.phone" class="promoter-card-phone">
|
||||
<SensitiveText :record="item" field="phone" :show-eye="false" />
|
||||
</div>
|
||||
<Tag class="promoter-card-role" :bordered="false">
|
||||
{{ item.role_name || '业务员' }}
|
||||
</Tag>
|
||||
|
||||
@@ -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(() => {
|
||||
<div class="store-card-name" :title="item.name">
|
||||
{{ item.name || '-' }}
|
||||
</div>
|
||||
<div v-if="item.mobile" class="store-card-sub">{{ item.mobile }}</div>
|
||||
<div v-if="item.mobile" class="store-card-sub">
|
||||
<SensitiveText :record="item" field="mobile" :show-eye="false" />
|
||||
</div>
|
||||
<div class="store-card-sub">ID: {{ item.id }}</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -149,7 +152,13 @@ onMounted(() => {
|
||||
<div class="store-selected-info">
|
||||
<div class="store-selected-name">{{ selectedOption.name }}</div>
|
||||
<div class="store-selected-sub">
|
||||
{{ selectedOption.mobile || `ID: ${selectedOption.id}` }}
|
||||
<SensitiveText
|
||||
v-if="selectedOption.mobile"
|
||||
:record="selectedOption"
|
||||
field="mobile"
|
||||
:show-eye="false"
|
||||
/>
|
||||
<span v-else>ID: {{ selectedOption.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import { Avatar, Descriptions, Spin } from 'ant-design-vue';
|
||||
|
||||
import { getPromoterDetailApi } from '#/views/system/store-input/api';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
|
||||
|
||||
type PromoterDetail = {
|
||||
@@ -62,7 +63,9 @@ async function loadDetail(id?: number, code?: string) {
|
||||
</div>
|
||||
<Descriptions bordered :column="1" size="small" class="promoter-detail__desc">
|
||||
<Descriptions.Item label="推广码">{{ detail.code || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">{{ detail.phone || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">
|
||||
<SensitiveText :record="detail" field="phone" />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="录入诊所">
|
||||
{{ detail.clinic_input_count ?? 0 }} 家
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -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 }}
|
||||
</div>
|
||||
<div v-else-if="admin.phone" class="promoter-info-card__sub">
|
||||
{{ admin.phone }}
|
||||
<SensitiveText :record="admin" field="phone" :show-eye="false" />
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
|
||||
158
apps/web-antd/src/components/sensitive-text/SensitiveText.vue
Normal file
158
apps/web-antd/src/components/sensitive-text/SensitiveText.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<script lang="ts" setup>
|
||||
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<string, unknown> | 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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="sensitive-text">
|
||||
<span
|
||||
class="sensitive-text__value"
|
||||
:class="{ 'sensitive-text__value--copyable': copyable && hasValue }"
|
||||
@click="handleCopyText"
|
||||
>
|
||||
{{ displayText }}
|
||||
</span>
|
||||
<EyeOutlined
|
||||
v-if="showEye && hasValue && !revealed"
|
||||
class="sensitive-text__eye"
|
||||
@click.stop="toggleReveal"
|
||||
/>
|
||||
<EyeInvisibleOutlined
|
||||
v-else-if="showEye && hasValue && revealed"
|
||||
class="sensitive-text__eye"
|
||||
@click.stop="toggleReveal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sensitive-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sensitive-text__value {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sensitive-text__value--copyable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sensitive-text__value--copyable:hover {
|
||||
color: var(--ant-color-primary, #1677ff);
|
||||
}
|
||||
|
||||
.sensitive-text__eye {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.sensitive-text__eye :deep(svg) {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.sensitive-text__eye:hover {
|
||||
color: var(--ant-color-primary, #1677ff);
|
||||
}
|
||||
|
||||
/* 暗色模式:避免 fallback 黑色导致图标不可见 */
|
||||
.dark .sensitive-text__eye {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.dark .sensitive-text__eye:hover {
|
||||
color: var(--ant-color-primary, #4096ff);
|
||||
}
|
||||
|
||||
.dark .sensitive-text__value--copyable:hover {
|
||||
color: var(--ant-color-primary, #4096ff);
|
||||
}
|
||||
</style>
|
||||
133
apps/web-antd/src/constants/sensitive-fields.ts
Normal file
133
apps/web-antd/src/constants/sensitive-fields.ts
Normal file
@@ -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<string, unknown> | null;
|
||||
field: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为敏感字段
|
||||
*/
|
||||
export function isSensitiveField(field: string): boolean {
|
||||
return MASK_DISPLAY_FIELDS.has(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 record 读取明文,支持 fallback 链
|
||||
*/
|
||||
export function getSensitivePlainText(
|
||||
record: Record<string, unknown> | 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<string, unknown> | 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<string, unknown> | null | undefined,
|
||||
field: string,
|
||||
): string {
|
||||
if (!record) {
|
||||
return '';
|
||||
}
|
||||
const value = record[field];
|
||||
if (value == null || value === '') {
|
||||
return '';
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
@@ -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) {
|
||||
<template v-if="info">
|
||||
<Card class="mb-4" title="基本信息">
|
||||
<p>姓名:{{ info.salesperson?.nick_name }}</p>
|
||||
<p>手机:{{ info.salesperson?.phone }}</p>
|
||||
<p>手机:<SensitiveText :record="info.salesperson" field="phone" /></p>
|
||||
<p>所属诊所:{{ info.store?.name }}</p>
|
||||
<div v-if="info.qr_code" class="mt-3">
|
||||
<p class="mb-2">推广二维码</p>
|
||||
|
||||
@@ -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) {
|
||||
</div>
|
||||
|
||||
<div class="mb-3 space-y-1 text-sm text-gray-600">
|
||||
<div>{{ item.phone }}</div>
|
||||
<div><SensitiveText :record="item" field="phone" /></div>
|
||||
<div v-if="item.parent_nick_name" class="text-xs text-gray-500">
|
||||
邀请人:{{ item.parent_nick_name }}
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="联系电话">
|
||||
{{ data.address?.mobile || data.patient_mobile }}
|
||||
<SensitiveText
|
||||
:record="data.address"
|
||||
field="mobile"
|
||||
:fallback="{ record: data, field: 'patient_mobile' }"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="deliveryMethod === 0" label="配送地址">
|
||||
{{
|
||||
|
||||
@@ -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 = () => {
|
||||
}}
|
||||
</div>
|
||||
<div class="text-gray-500">
|
||||
{{ row.address?.mobile || row.patient_mobile || '—' }}
|
||||
<SensitiveText
|
||||
:record="row.address"
|
||||
field="mobile"
|
||||
:fallback="{ record: row, field: 'patient_mobile' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">{{ formatExpressAddress(row) }}</div>
|
||||
</div>
|
||||
|
||||
@@ -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 });
|
||||
<Tag :color="statusColor">{{ detail.status_text || '—' }}</Tag>
|
||||
<Tag>{{ detail.transfer_mode_text || '—' }}</Tag>
|
||||
</div>
|
||||
<div>患者:{{ detail.patient_name }} {{ detail.patient_mobile }}</div>
|
||||
<div class="flex items-center gap-1 flex-wrap">
|
||||
<span>患者:{{ detail.patient_name }}</span>
|
||||
<SensitiveText :record="detail" field="patient_mobile" :show-eye="false" />
|
||||
</div>
|
||||
<div>地址:{{ detail.patient_address || '—' }}</div>
|
||||
<div v-if="detail.remark">备注:{{ detail.remark }}</div>
|
||||
<div v-if="detail.status === 3 && detail.exception_message" class="text-red-500">
|
||||
|
||||
@@ -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: '方式',
|
||||
|
||||
@@ -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) {
|
||||
</div>
|
||||
|
||||
<div class="mb-4 space-y-1.5 text-sm text-gray-600">
|
||||
<div>{{ item.phone }}</div>
|
||||
<div><SensitiveText :record="item" field="phone" /></div>
|
||||
<div v-if="item.parent_nick_name" class="text-xs text-gray-500">
|
||||
邀请人:{{ item.parent_nick_name }}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { h, ref, watch } from 'vue';
|
||||
|
||||
import { Drawer, Table } from 'ant-design-vue';
|
||||
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import { invitedSalespersonListApi } from '#/views/business/store/settings/api';
|
||||
|
||||
const open = ref(false);
|
||||
@@ -13,7 +14,13 @@ const list = ref<any[]>([]);
|
||||
|
||||
const columns = [
|
||||
{ title: '名称', dataIndex: 'nick_name', key: 'nick_name', width: 120 },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 120 },
|
||||
{
|
||||
title: '手机号',
|
||||
key: 'phone',
|
||||
width: 120,
|
||||
customRender: ({ record }: any) =>
|
||||
h(SensitiveText, { record, field: 'phone', showEye: false }),
|
||||
},
|
||||
{ title: '邀请分成', dataIndex: 'inviter_split', key: 'inviter_split', width: 90 },
|
||||
{ title: '待结算', dataIndex: 'invite_pending_amount', key: 'pending', width: 100 },
|
||||
{ title: '已结算', dataIndex: 'invite_settled_amount', key: 'settled', width: 100 },
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computed, ref, watch } from 'vue';
|
||||
import { Avatar, Drawer, Empty, Input, Spin } from 'ant-design-vue';
|
||||
|
||||
import { salespersonOptionsApi } from '#/views/business/store/settings/api';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -105,7 +106,7 @@ watch(
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate font-medium">{{ displayName }}</div>
|
||||
<div v-if="selectedItem?.phone" class="text-xs text-gray-500">
|
||||
{{ selectedItem.phone }}
|
||||
<SensitiveText :record="selectedItem" field="phone" :show-eye="false" />
|
||||
</div>
|
||||
</div>
|
||||
<a class="text-xs text-gray-400" @click.stop="clear">清空</a>
|
||||
@@ -131,7 +132,9 @@ watch(
|
||||
<Avatar v-else :size="40">{{ (item.nick_name || '?').charAt(0) }}</Avatar>
|
||||
<div>
|
||||
<div class="font-medium">{{ item.nick_name }}</div>
|
||||
<div class="text-xs text-gray-500">{{ item.phone }}</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
<SensitiveText :record="item" field="phone" :show-eye="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
updateNavSortApi,
|
||||
updateClinicTypeApi,
|
||||
} from '#/views/business/store/settings/api';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
import UploadModal from './components/uploadModal.vue';
|
||||
import SalespersonCard from "#/views/business/store/settings/components/SalespersonCard.vue";
|
||||
@@ -391,7 +392,7 @@ const handleSwitchClinicType = () => {
|
||||
{{ data.contact }}
|
||||
</p>
|
||||
<p class="flex items-center gap-2">
|
||||
{{ data.mobile }}
|
||||
<SensitiveText :record="data" field="mobile" />
|
||||
</p>
|
||||
<p class="flex items-center gap-2">
|
||||
入驻时间:{{ formatDate(data.created_at) }}
|
||||
@@ -571,7 +572,7 @@ const handleSwitchClinicType = () => {
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
手机号:
|
||||
<span>{{ doctor.doctor.mobile }}</span>
|
||||
<SensitiveText :record="doctor.doctor" field="mobile" />
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
专业:
|
||||
|
||||
@@ -12,6 +12,24 @@ export const formOptions: VbenFormProps = {
|
||||
fieldName: 'name',
|
||||
label: '药名称',
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
placeholder: '选择状态',
|
||||
options: [
|
||||
{
|
||||
label: '下架',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '上架',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '上架状态',
|
||||
},
|
||||
],
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ref, watch } from 'vue';
|
||||
import { Button, Drawer, Empty, List, message, Tag } from 'ant-design-vue';
|
||||
|
||||
import { getSalespersonTransferByRegisterApi } from '#/views/business/salesperson-transfer-prescription/api';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import { parseSalespersonTransferDrugList } from '#/views/doctor/doctor-reception/utils/parseSalespersonTransferDrugList';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -77,9 +78,13 @@ function close() {
|
||||
<template #renderItem="{ item }">
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
:title="`${item.patient_name || ''} ${item.patient_mobile || ''}`"
|
||||
:description="`推广员:${item.salesperson_name || '-'} · ${item.transfer_time_text || ''}`"
|
||||
/>
|
||||
>
|
||||
<template #title>
|
||||
<span>{{ item.patient_name || '' }} </span>
|
||||
<SensitiveText :record="item" field="patient_mobile" :show-eye="false" />
|
||||
</template>
|
||||
</List.Item.Meta>
|
||||
<template #actions>
|
||||
<Tag v-if="matchTypeLabel(item.match_type)" :color="item.match_type === 'mobile' ? 'success' : 'processing'">
|
||||
{{ matchTypeLabel(item.match_type) }}
|
||||
|
||||
@@ -66,6 +66,7 @@ import CommonPrescriptionModal from './components/CommonPrescriptionModal.vue';
|
||||
import ConfirmModal from './components/ConfirmModal.vue';
|
||||
// 信息提示模态框组件
|
||||
import InfoModal from '#/components/modal/InfoModal.vue';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
// 药品搜索选择组件
|
||||
import { DrugSearchSelect } from '#/components/drug-search-select';
|
||||
// 保存常用方API
|
||||
@@ -2459,7 +2460,9 @@ watch(
|
||||
}}
|
||||
</Tag>
|
||||
</h3>
|
||||
<p class="phone">{{ patient.user_patient.mobile }}</p>
|
||||
<p class="phone">
|
||||
<SensitiveText :record="patient.user_patient" field="mobile" :show-eye="false" />
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
推广员:{{ patient.salesperson?.nick_name || '无' }}
|
||||
</p>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
|
||||
import AvatarUpload from '#/components/form/components/avatar.vue';
|
||||
import DoctorQrCodePreview from '#/components/modal/DoctorQrCodePreview.vue';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
|
||||
import OrderDetail from '#/views/business/order/product-order/components/detail.vue';
|
||||
import { openPcWindowsApi, updateDoctor } from '../api';
|
||||
@@ -526,7 +527,9 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</div>
|
||||
</Form>
|
||||
<Descriptions v-else bordered :column="2" size="middle">
|
||||
<Descriptions.Item label="登录手机号">{{ serviceUserForm.mobile || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="登录手机号">
|
||||
<SensitiveText :record="serviceUserForm" field="mobile" />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="昵称">{{ serviceUserForm.nickname || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="账号状态">
|
||||
<Tag :color="cardData?.service_user?.status === 2 ? 'success' : 'default'">
|
||||
@@ -631,7 +634,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</p>
|
||||
<p class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /></svg>
|
||||
<span>{{ item.store?.contact }} ({{ item.store?.mobile }})</span>
|
||||
<span>{{ item.store?.contact }} (<SensitiveText :record="item.store" field="mobile" />)</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -656,7 +659,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
<h3 class="text-xl font-bold text-gray-800 dark:text-slate-100 mb-1">{{ cardData.admin.nick_name }}</h3>
|
||||
<p class="text-gray-500 dark:text-slate-400 mb-4 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
|
||||
{{ cardData.admin.phone }}
|
||||
<SensitiveText :record="cardData.admin" field="phone" />
|
||||
</p>
|
||||
<Tag color="processing">PC 端权限已开通</Tag>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'name', align: 'left', title: '名称', slots: { default: 'name' } },
|
||||
{ field: 'mobile', align: 'left', title: '手机号' },
|
||||
{ field: 'mobile', align: 'left', title: '手机号', slots: { default: 'mobile' } },
|
||||
{
|
||||
field: 'avatar',
|
||||
align: 'left',
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Button, Image, message, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
import { auditApi, openPcWindowsApi, refuseApi } from './api';
|
||||
import BindStore from './components/BindStoreModal.vue';
|
||||
@@ -160,6 +161,9 @@ const refuse = (id: number) => {
|
||||
{{ row.name }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #mobile="{ row }">
|
||||
<SensitiveText :record="row" field="mobile" />
|
||||
</template>
|
||||
<template #stores="{ row }">
|
||||
<p v-for="item in row.stores" :key="item.store_id">
|
||||
{{ item?.store?.name }}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { sendMessage } from '#/views/business/chat/utils/request';
|
||||
import { useUserStore } from '#/views/business/chat/stores/user';
|
||||
import { getTransferPrescriptionByRegisterApi } from '#/views/business/chat/api/transferPrescription';
|
||||
import TransferPrescriptionCard from '#/views/doctor/online-consultation/components/TransferPrescriptionCard.vue';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import { usePrescriptionStore } from '#/store/prescription';
|
||||
|
||||
const userStore = useUserStore();
|
||||
@@ -206,7 +207,12 @@ watch(
|
||||
{{ getSexText(patientDetail.user_patient?.sex) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">
|
||||
{{ patientDetail.user_patient?.mobile || patientDetail.user?.mobile || '未填写' }}
|
||||
<SensitiveText
|
||||
:record="patientDetail.user_patient"
|
||||
field="mobile"
|
||||
:fallback="{ record: patientDetail.user, field: 'mobile' }"
|
||||
empty-text="未填写"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="patientDetail.register_order?.order_no" label="订单号">
|
||||
{{ patientDetail.register_order.order_no }}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, watch, onMounted } from 'vue';
|
||||
import { RadioButton, RadioGroup, Button, message } from 'ant-design-vue';
|
||||
import { ReloadOutlined } from '@ant-design/icons-vue';
|
||||
import { getPatientList } from '../api/index.ts';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import { useChatStore } from '#/views/business/chat/stores/chat';
|
||||
|
||||
const chatStore = useChatStore();
|
||||
@@ -161,7 +162,14 @@ defineExpose({
|
||||
<h3 class="patient-name">
|
||||
{{ patient.user_patient?.name || patient.user?.nick_name || '未知患者' }}
|
||||
</h3>
|
||||
<p class="patient-phone">{{ patient.user_patient?.mobile || patient.user?.mobile || '' }}</p>
|
||||
<p class="patient-phone">
|
||||
<SensitiveText
|
||||
:record="patient.user_patient"
|
||||
field="mobile"
|
||||
:fallback="{ record: patient.user, field: 'mobile' }"
|
||||
:show-eye="false"
|
||||
/>
|
||||
</p>
|
||||
<p v-if="patient.order_no" class="order-no">订单号:{{ patient.order_no }}</p>
|
||||
<div class="patient-meta">
|
||||
<span :class="['status', getStatusClass(patient.status)]">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { sendMessage } from '#/views/business/chat/utils/request';
|
||||
import { useUserStore } from '#/views/business/chat/stores/user';
|
||||
import { getTransferPrescriptionByRegisterApi } from '#/views/business/chat/api/transferPrescription';
|
||||
import TransferPrescriptionCard from './TransferPrescriptionCard.vue';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import { usePrescriptionStore } from '#/store/prescription';
|
||||
|
||||
const userStore = useUserStore();
|
||||
@@ -206,7 +207,12 @@ watch(
|
||||
{{ getSexText(patientDetail.user_patient?.sex) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">
|
||||
{{ patientDetail.user_patient?.mobile || patientDetail.user?.mobile || '未填写' }}
|
||||
<SensitiveText
|
||||
:record="patientDetail.user_patient"
|
||||
field="mobile"
|
||||
:fallback="{ record: patientDetail.user, field: 'mobile' }"
|
||||
empty-text="未填写"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="patientDetail.register_order?.order_no" label="订单号">
|
||||
{{ patientDetail.register_order.order_no }}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, watch, onMounted } from 'vue';
|
||||
import { RadioButton, RadioGroup, Button, message } from 'ant-design-vue';
|
||||
import { ReloadOutlined } from '@ant-design/icons-vue';
|
||||
import { getPatientList } from '../api/index.ts';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import { useChatStore } from '#/views/business/chat/stores/chat';
|
||||
|
||||
const chatStore = useChatStore();
|
||||
@@ -170,7 +171,14 @@ defineExpose({
|
||||
<h3 class="patient-name">
|
||||
{{ patient.user_patient?.name || patient.user?.nick_name || '未知患者' }}
|
||||
</h3>
|
||||
<p class="patient-phone">{{ patient.user_patient?.mobile || patient.user?.mobile || '' }}</p>
|
||||
<p class="patient-phone">
|
||||
<SensitiveText
|
||||
:record="patient.user_patient"
|
||||
field="mobile"
|
||||
:fallback="{ record: patient.user, field: 'mobile' }"
|
||||
:show-eye="false"
|
||||
/>
|
||||
</p>
|
||||
<p v-if="patient.order_no" class="order-no">订单号:{{ patient.order_no }}</p>
|
||||
<div class="patient-meta">
|
||||
<div class="meta-left">
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import { UserOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
import {
|
||||
getPatientDetailApi,
|
||||
@@ -442,10 +443,18 @@ function getSexText(sex: number): string {
|
||||
{{ patientDetail.age }}岁
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">
|
||||
{{ patientDetail.patient?.mobile || '未填写' }}
|
||||
<SensitiveText
|
||||
:record="patientDetail.patient"
|
||||
field="mobile"
|
||||
empty-text="未填写"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="身份证" :span="2">
|
||||
{{ patientDetail.patient?.id_card || '未填写' }}
|
||||
<SensitiveText
|
||||
:record="patientDetail.patient"
|
||||
field="id_card"
|
||||
empty-text="未填写"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
type PageResponse,
|
||||
} from './api';
|
||||
import PatientDetailModal from './components/PatientDetailModal.vue';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import SendMessageModal from './components/SendMessageModal.vue';
|
||||
|
||||
// ==================== 响应式数据 ====================
|
||||
@@ -341,7 +342,7 @@ onMounted(() => {
|
||||
>
|
||||
</div>
|
||||
<p class="patient-mobile" v-if="patient.mobile">
|
||||
{{ patient.mobile }}
|
||||
<SensitiveText :record="patient" field="mobile" :show-eye="false" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import { getPharmacistInfo } from '../api';
|
||||
import { Image } from 'ant-design-vue';
|
||||
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
const data = ref();
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
@@ -20,18 +22,6 @@ const formatDate = (dateStr: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 身份证号脱敏处理
|
||||
const formatIdCard = (idCard: string) => {
|
||||
if (!idCard) return '';
|
||||
try {
|
||||
// 假设需要Base64解码(根据实际情况调整)
|
||||
const decoded = atob(idCard);
|
||||
return `${decoded.slice(0, 6)}********${decoded.slice(14)}`;
|
||||
} catch {
|
||||
return idCard;
|
||||
}
|
||||
};
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
@@ -140,12 +130,12 @@ const qualificationImages = computed(() => {
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="w-24 text-sm font-medium text-gray-600">手机号:</span>
|
||||
<span class="text-gray-700">{{ data.mobile }}</span>
|
||||
<SensitiveText :record="data" field="mobile" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="w-24 text-sm font-medium text-gray-600">身份证号:</span>
|
||||
<SensitiveText :record="data" field="idcard" empty-text="未填写" />
|
||||
</div>
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="w-24 text-sm font-medium text-gray-600">身份证号:</span>-->
|
||||
<!-- <span class="text-gray-700">{{ data.idcard }}</span>-->
|
||||
<!-- </div>-->
|
||||
<div class="flex items-center">
|
||||
<span class="w-24 text-sm font-medium text-gray-600">门店ID:</span>
|
||||
<span class="text-gray-700">{{ data.store_id }}</span>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'name', align: 'left', title: '名称' },
|
||||
{ field: 'mobile', align: 'left', title: '手机号' },
|
||||
{ field: 'mobile', align: 'left', title: '手机号', slots: { default: 'mobile' } },
|
||||
{
|
||||
field: 'avatar',
|
||||
align: 'left',
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Button, Image, message, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
import { auditApi, openPcWindowsApi, refuseApi } from './api';
|
||||
import BindStore from './components/BindStoreModal.vue';
|
||||
@@ -140,6 +141,9 @@ const refuse = (id: number) => {
|
||||
<template #avatar="{ row }">
|
||||
<Image :src="row.avatar" height="30" width="30" />
|
||||
</template>
|
||||
<template #mobile="{ row }">
|
||||
<SensitiveText :record="row" field="mobile" />
|
||||
</template>
|
||||
<template #stores="{ row }">
|
||||
<p v-for="item in row.bind_stores" :key="item.id">
|
||||
{{ item?.name }}
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
saveServicePriceApi,
|
||||
} from '#/views/doctor/settings/api';
|
||||
import type { CommonPrescriptionResponse } from '#/views/doctor/settings/api';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
const userInfoStore = useUserStore();
|
||||
|
||||
@@ -375,7 +376,7 @@ const handleViewSignature = () => {
|
||||
</h1>
|
||||
<p class="flex items-center gap-2">
|
||||
<PhoneIcon class="h-5 w-5" />
|
||||
{{ data.mobile }}
|
||||
<SensitiveText :record="data" field="mobile" />
|
||||
</p>
|
||||
<p class="flex items-center gap-2">
|
||||
<CalendarIcon class="h-5 w-5" />
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Button, Image, message, Avatar } from 'ant-design-vue';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import {
|
||||
createAdmin,
|
||||
deleteAdmin,
|
||||
@@ -293,13 +294,16 @@ const copyToClipboard = async (text: string) => {
|
||||
{{ row.doctor_profile?.name || `su_id: ${row.doctor_id}` }}
|
||||
</div>
|
||||
<div v-if="row.doctor_profile?.mobile" class="text-xs text-gray-400">
|
||||
{{ row.doctor_profile.mobile }}
|
||||
<SensitiveText :record="row.doctor_profile" field="mobile" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template #phone="{ row }">
|
||||
<SensitiveText :record="row" field="phone" />
|
||||
</template>
|
||||
<template #login_account="{ row }">
|
||||
<span v-if="row.login_account">
|
||||
<Button
|
||||
|
||||
@@ -88,7 +88,7 @@ function buildColumns(formType: AdminFormType) {
|
||||
}
|
||||
|
||||
cols.push(
|
||||
{ field: 'phone', title: '手机号码' },
|
||||
{ field: 'phone', title: '手机号码', slots: { default: 'phone' } },
|
||||
{
|
||||
field: 'login_account',
|
||||
align: 'left',
|
||||
|
||||
@@ -44,7 +44,7 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
{ field: 'code', title: '业务推广码' },
|
||||
{ field: 'platform.name', title: '所属平台' },
|
||||
{ field: 'supplier.name', title: '所属供应商' },
|
||||
{ field: 'phone', title: '手机号码' },
|
||||
{ field: 'phone', title: '手机号码', slots: { default: 'phone' } },
|
||||
{
|
||||
field: 'login_account',
|
||||
align: 'left',
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Avatar, Descriptions, DescriptionsItem, Image, Spin, message } from 'ant-design-vue';
|
||||
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
import { getDoctorInputDetail } from '../api';
|
||||
import {
|
||||
inputUserDisplayName,
|
||||
@@ -72,8 +74,12 @@ const [Modal, modalApi] = useVbenModal({
|
||||
<Descriptions :column="2" bordered>
|
||||
<DescriptionsItem label="ID">{{ detailData.id }}</DescriptionsItem>
|
||||
<DescriptionsItem label="医生姓名">{{ detailData.name }}</DescriptionsItem>
|
||||
<DescriptionsItem label="手机号">{{ detailData.mobile }}</DescriptionsItem>
|
||||
<DescriptionsItem label="身份证">{{ detailData.idcard }}</DescriptionsItem>
|
||||
<DescriptionsItem label="手机号">
|
||||
<SensitiveText :record="detailData" field="mobile" />
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="身份证">
|
||||
<SensitiveText :record="detailData" field="idcard" />
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="身份">{{ getTypeText(detailData.type) }}</DescriptionsItem>
|
||||
<DescriptionsItem label="签名类型">{{ getSignTypeText(detailData.sign_type) }}</DescriptionsItem>
|
||||
<DescriptionsItem label="科室">{{ detailData.depart?.name || '-' }}</DescriptionsItem>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const inputGridOptions: VxeGridProps<RowType> = {
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'name', align: 'left', title: '医生姓名' },
|
||||
{ field: 'mobile', title: '手机号', width: 140 },
|
||||
{ field: 'mobile', title: '手机号', width: 140, slots: { default: 'mobile' } },
|
||||
{
|
||||
field: 'type',
|
||||
align: 'left',
|
||||
@@ -111,7 +111,7 @@ export const auditGridOptions: VxeGridProps<RowType> = {
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'name', align: 'left', title: '医生姓名' },
|
||||
{ field: 'mobile', title: '手机号', width: 140 },
|
||||
{ field: 'mobile', title: '手机号', width: 140, slots: { default: 'mobile' } },
|
||||
{
|
||||
field: 'type',
|
||||
align: 'left',
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Avatar, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
import DetailModal from './components/DetailModal.vue';
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
@@ -119,6 +120,9 @@ const getStatusText = (status: number) => {
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template #mobile="{ row }">
|
||||
<SensitiveText :record="row" field="mobile" />
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { Icon } from '#/components/icon';
|
||||
import QrCodePreview from '#/components/modal/QrCodePreview.vue';
|
||||
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
// 导入药店管理相关的API(直接使用store的API)
|
||||
@@ -478,8 +479,7 @@ const batchSyncDrugPrice = () => {
|
||||
<Button type="link" @click="copyText(row.contact)">复制</Button>
|
||||
</div>
|
||||
<div>
|
||||
号码:{{ row.mobile }}
|
||||
<Button type="link" @click="copyText(row.mobile)">复制</Button>
|
||||
号码:<SensitiveText :record="row" field="mobile" />
|
||||
</div>
|
||||
</template>
|
||||
<template #qr_code="{ row }">
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
import { getStoreInputDetail } from '../api';
|
||||
import DoctorInputCardGrid from './DoctorInputCardGrid.vue';
|
||||
import { getLinkedDoctors } from '../utils/doctorInputDisplay';
|
||||
@@ -86,7 +88,9 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="联系人">{{ detailData.contact }}</DescriptionsItem>
|
||||
<DescriptionsItem label="联系电话">{{ detailData.mobile }}</DescriptionsItem>
|
||||
<DescriptionsItem label="联系电话">
|
||||
<SensitiveText :record="detailData" field="mobile" />
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="省市区">
|
||||
{{ detailData.province?.name }} {{ detailData.city?.name }}
|
||||
</DescriptionsItem>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
Popover,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
import { resolveAvatarUrl } from '../utils/inputUser';
|
||||
import {
|
||||
doctorCertImages,
|
||||
@@ -98,8 +100,12 @@ function cardClass(doc: Record<string, any>) {
|
||||
<Descriptions :column="1" size="small" class="max-w-[480px]">
|
||||
<DescriptionsItem label="ID">{{ doc.id }}</DescriptionsItem>
|
||||
<DescriptionsItem label="姓名">{{ doc.name }}</DescriptionsItem>
|
||||
<DescriptionsItem label="手机号">{{ doc.mobile || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="身份证">{{ doc.idcard || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="手机号">
|
||||
<SensitiveText :record="doc" field="mobile" />
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="身份证">
|
||||
<SensitiveText :record="doc" field="idcard" />
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="身份">{{ getDoctorTypeText(doc.type) }}</DescriptionsItem>
|
||||
<DescriptionsItem label="签名类型">
|
||||
{{ getSignTypeText(doc.sign_type) }}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const inputGridOptions: VxeGridProps<RowType> = {
|
||||
slots: { default: 'status' },
|
||||
},
|
||||
{ field: 'contact', title: '联系人' },
|
||||
{ field: 'mobile', title: '联系电话' },
|
||||
{ field: 'mobile', title: '联系电话', slots: { default: 'mobile' } },
|
||||
{ field: 'position', title: '详细地址' },
|
||||
{ field: 'created_at', title: '录入时间' },
|
||||
{
|
||||
@@ -159,7 +159,7 @@ export const auditGridOptions: VxeGridProps<RowType> = {
|
||||
},
|
||||
{ field: 'audit_time', title: '审核时间', slots: { default: 'audit_time' } },
|
||||
{ field: 'contact', title: '联系人' },
|
||||
{ field: 'mobile', title: '联系电话' },
|
||||
{ field: 'mobile', title: '联系电话', slots: { default: 'mobile' } },
|
||||
{ field: 'position', title: '详细地址' },
|
||||
{ field: 'created_at', title: '录入时间' },
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Avatar, Button, message, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
|
||||
|
||||
import {
|
||||
@@ -158,6 +159,9 @@ const getStatusText = (status: number) => {
|
||||
<template #promoter_admin="{ row }">
|
||||
<PromoterInfoCard :admin="row.promoter_admin" size="compact" />
|
||||
</template>
|
||||
<template #mobile="{ row }">
|
||||
<SensitiveText :record="row" field="mobile" />
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
|
||||
defineProps<{
|
||||
row: Record<string, any>;
|
||||
variant: 'clinic' | 'pharmacy';
|
||||
@@ -52,16 +54,12 @@ function getClinicTypeText(clinicType: number) {
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-item__label">管理员</span>
|
||||
<template v-if="row.store_admin?.phone">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="info-item__action"
|
||||
@click="onCopy(row.store_admin.phone)"
|
||||
>
|
||||
{{ row.store_admin.phone }}
|
||||
</Button>
|
||||
</template>
|
||||
<SensitiveText
|
||||
v-if="row.store_admin?.phone"
|
||||
:record="row.store_admin"
|
||||
field="phone"
|
||||
empty-text="-"
|
||||
/>
|
||||
<span v-else class="info-item__value">-</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { Icon } from '#/components/icon';
|
||||
import QrCodePreview from '#/components/modal/QrCodePreview.vue';
|
||||
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
|
||||
|
||||
@@ -510,8 +511,7 @@ const handleSwitchClinicType = (row: any) => {
|
||||
<Button type="link" @click="copyText(row.contact)">复制</Button>
|
||||
</div>
|
||||
<div>
|
||||
号码:{{ row.mobile }}
|
||||
<Button type="link" @click="copyText(row.mobile)">复制</Button>
|
||||
号码:<SensitiveText :record="row" field="mobile" />
|
||||
</div>
|
||||
</template>
|
||||
<template #qr_code="{ row }">
|
||||
|
||||
Reference in New Issue
Block a user