1. 特色方功能迭代
This commit is contained in:
261
common/js/content-security.js
Normal file
261
common/js/content-security.js
Normal file
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* 微信内容安全检查(医生端小程序)
|
||||
* 调用 xk-api /api/doctor/content-security/* 接口
|
||||
*/
|
||||
import { req } from '@/common/js/index.js';
|
||||
import { promptDoctorWxBind } from '@/common/js/wx-bind-helper.js';
|
||||
|
||||
/** 资料类场景(个人信息编辑) */
|
||||
export const SCENE_PROFILE = 1;
|
||||
/** 社交日志场景(聊天) */
|
||||
export const SCENE_CHAT = 4;
|
||||
|
||||
/** 微信 msg_sec_check 单条内容上限 */
|
||||
const MAX_TEXT_LEN = 2500;
|
||||
|
||||
/** 需先绑定微信的错误文案片段 */
|
||||
const NEED_BIND_MSGS = ['请先绑定微信', '用户未绑定微信', '无法获取用户手机号', '绑定微信'];
|
||||
|
||||
/**
|
||||
* 从多种 reject 结构中提取错误文案(兼容 HTTP 4xx 拦截器固定 msg)
|
||||
*/
|
||||
function extractErrorMessage(err) {
|
||||
if (!err) {
|
||||
return '';
|
||||
}
|
||||
const direct = err.msg || err.message;
|
||||
if (direct && direct !== 'response 300-499') {
|
||||
return String(direct);
|
||||
}
|
||||
const resData = err.res && err.res.data;
|
||||
if (resData == null) {
|
||||
return '';
|
||||
}
|
||||
if (typeof resData === 'object') {
|
||||
return String(resData.message || resData.msg || '');
|
||||
}
|
||||
if (typeof resData === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(resData);
|
||||
return String((parsed && (parsed.message || parsed.msg)) || '');
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为未绑定微信导致的检测失败
|
||||
*/
|
||||
export function isNeedBindError(err) {
|
||||
const msg = extractErrorMessage(err);
|
||||
return NEED_BIND_MSGS.some((item) => msg.includes(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* 未绑定时弹窗确认,用户同意则绑定并重试检测
|
||||
*/
|
||||
async function handleNeedBindAndRetry(retryFn) {
|
||||
const confirmed = await promptDoctorWxBind();
|
||||
if (!confirmed) {
|
||||
throw { msg: '已取消绑定,无法进行内容安全校验' };
|
||||
}
|
||||
return retryFn();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一展示内容安全错误
|
||||
*/
|
||||
export function handleSecurityError(err) {
|
||||
const msg = extractErrorMessage(err) || '内容含有违规信息,请修改后重试';
|
||||
uni.showToast({ title: msg, icon: 'none' });
|
||||
}
|
||||
|
||||
function assertCheckOk(res) {
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
return true;
|
||||
}
|
||||
throw { msg: (res && (res.message || res.msg)) || '内容含有违规信息,请修改后重试' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字段值规范为可比较的字符串(数组用逗号拼接)
|
||||
*/
|
||||
function normalizeFieldValue(val) {
|
||||
if (val == null) {
|
||||
return '';
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
return val.filter(Boolean).map((item) => String(item).trim()).filter(Boolean).join(',');
|
||||
}
|
||||
return String(val).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 baseline 对比,返回有变动且非空的文本列表
|
||||
* @param {Object} fields 当前表单字段 { key: value }
|
||||
* @param {Object} baseline 初始快照
|
||||
*/
|
||||
export function pickChangedTexts(fields, baseline = {}) {
|
||||
const texts = [];
|
||||
const base = baseline || {};
|
||||
Object.keys(fields || {}).forEach((key) => {
|
||||
const current = normalizeFieldValue(fields[key]);
|
||||
const original = normalizeFieldValue(base[key]);
|
||||
if (current && current !== original) {
|
||||
texts.push(current);
|
||||
}
|
||||
});
|
||||
return texts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多条文本用换行拼接;超过 2500 字则按字段拆成多批请求
|
||||
*/
|
||||
export function joinTextsForCheck(texts) {
|
||||
const list = (Array.isArray(texts) ? texts : [])
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean);
|
||||
if (!list.length) {
|
||||
return [];
|
||||
}
|
||||
const batches = [];
|
||||
let current = '';
|
||||
list.forEach((text) => {
|
||||
const merged = current ? `${current}\n${text}` : text;
|
||||
if (merged.length <= MAX_TEXT_LEN) {
|
||||
current = merged;
|
||||
return;
|
||||
}
|
||||
if (current) {
|
||||
batches.push(current);
|
||||
current = '';
|
||||
}
|
||||
if (text.length <= MAX_TEXT_LEN) {
|
||||
current = text;
|
||||
} else {
|
||||
// 单字段超长时按上限切分(就诊人场景几乎不会触发)
|
||||
for (let i = 0; i < text.length; i += MAX_TEXT_LEN) {
|
||||
batches.push(text.slice(i, i + MAX_TEXT_LEN));
|
||||
}
|
||||
}
|
||||
});
|
||||
if (current) {
|
||||
batches.push(current);
|
||||
}
|
||||
return batches;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单提交前:仅检测变动字段,合并为一次或少量 msg_sec_check 调用
|
||||
*/
|
||||
export async function checkProfileTexts(fields, baseline, scene = SCENE_PROFILE) {
|
||||
const changed = pickChangedTexts(fields, baseline);
|
||||
if (!changed.length) {
|
||||
return true;
|
||||
}
|
||||
const batches = joinTextsForCheck(changed);
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
await checkText(batches[i], scene);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function requestCheckText(content, scene) {
|
||||
const text = String(content || '').trim();
|
||||
if (!text) {
|
||||
return true;
|
||||
}
|
||||
const res = await req.request({
|
||||
url: '/newApi/content-security/check-text',
|
||||
method: 'POST',
|
||||
data: { content: text, scene },
|
||||
});
|
||||
return assertCheckOk(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本内容安全检查(遇未绑定微信时弹窗确认后绑定并重试一次)
|
||||
*/
|
||||
export async function checkText(content, scene = SCENE_PROFILE) {
|
||||
try {
|
||||
return await requestCheckText(content, scene);
|
||||
} catch (e) {
|
||||
if (isNeedBindError(e)) {
|
||||
return handleNeedBindAndRetry(() => requestCheckText(content, scene));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量文本检查(按顺序,每条独立请求;聊天等场景不适用合并检测)
|
||||
*/
|
||||
export async function checkTexts(texts, scene = SCENE_PROFILE) {
|
||||
const list = Array.isArray(texts) ? texts : [texts];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
if (item) {
|
||||
await checkText(item, scene);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function requestCheckImage(filePath, scene) {
|
||||
return true;
|
||||
const res = await req.request({
|
||||
url: '/newApi/content-security/check-image',
|
||||
type: 'upload',
|
||||
method: 'POST',
|
||||
filePath,
|
||||
name: 'file',
|
||||
formData: { scene: String(scene) },
|
||||
});
|
||||
return assertCheckOk(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地图片安全检查(遇未绑定微信时弹窗确认后绑定并重试一次)
|
||||
*/
|
||||
export async function checkImage(filePath, scene = SCENE_PROFILE) {
|
||||
if (!filePath) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return await requestCheckImage(filePath, scene);
|
||||
} catch (e) {
|
||||
if (isNeedBindError(e)) {
|
||||
return handleNeedBindAndRetry(() => requestCheckImage(filePath, scene));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestCheckImageUrl(url, scene) {
|
||||
const res = await req.request({
|
||||
url: '/newApi/content-security/check-image-url',
|
||||
method: 'POST',
|
||||
data: { url, scene },
|
||||
});
|
||||
return assertCheckOk(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 公网图片 URL 检查(遇未绑定微信时弹窗确认后绑定并重试一次)
|
||||
*/
|
||||
export async function checkImageUrl(url, scene = SCENE_PROFILE) {
|
||||
const imageUrl = String(url || '').trim();
|
||||
if (!imageUrl) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return await requestCheckImageUrl(imageUrl, scene);
|
||||
} catch (e) {
|
||||
if (isNeedBindError(e)) {
|
||||
return handleNeedBindAndRetry(() => requestCheckImageUrl(imageUrl, scene));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
* 例如 https://xiaokang88.oss-cn-hangzhou.aliyuncs.com
|
||||
*/
|
||||
import { getBusinessOssSignatureApi, unwrapOssSignature, registerBusinessOssFile } from '@/api/upload.js';
|
||||
import { checkImage } from '@/common/js/content-security.js';
|
||||
|
||||
/**
|
||||
* @param {string} filePath 本地临时文件路径
|
||||
@@ -59,6 +60,8 @@ export async function uploadToOss(options) {
|
||||
throw new Error('文件路径为空');
|
||||
}
|
||||
|
||||
await checkImage(filePath, 1);
|
||||
|
||||
const signature = await getOssSignatureFromBackend();
|
||||
const objectName = generateObjectName(filePath);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { prepareImagePath } from '@/common/js/image-compress.js';
|
||||
import { checkImage, handleSecurityError, SCENE_PROFILE } from '@/common/js/content-security.js';
|
||||
|
||||
export function chooseAvatarImage(options = {}) {
|
||||
const {
|
||||
@@ -29,6 +30,15 @@ export function chooseAvatarImage(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await checkImage(prepared.path, SCENE_PROFILE);
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
handleSecurityError(error);
|
||||
onFail && onFail(error);
|
||||
return;
|
||||
}
|
||||
|
||||
uni.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath: prepared.path,
|
||||
|
||||
94
common/js/wx-bind-helper.js
Normal file
94
common/js/wx-bind-helper.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 医生端微信绑定辅助方法
|
||||
* 供 wx-bind-entry 与内容安全自动绑定复用,避免重复 uni.login + bind 逻辑
|
||||
*/
|
||||
import { bindDoctorWxWechat } from '@/api/doctorAuth.js';
|
||||
|
||||
/**
|
||||
* 调用 uni.login 获取微信 code
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function getWxLoginCode() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (loginRes) => {
|
||||
if (!loginRes.code) {
|
||||
reject({ msg: '获取微信授权失败' });
|
||||
return;
|
||||
}
|
||||
resolve(loginRes.code);
|
||||
},
|
||||
fail: () => reject({ msg: '微信授权失败' }),
|
||||
});
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
reject({ msg: '请在微信小程序中使用' });
|
||||
// #endif
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保当前医生账号已绑定微信 openid(内容安全等接口依赖)
|
||||
* @param {Object} options
|
||||
* @param {boolean} options.showLoading 是否展示 loading,组件内已有 loading 时可传 false
|
||||
* @returns {Promise<boolean>} 绑定成功返回 true
|
||||
*/
|
||||
export async function ensureDoctorWxBound(options = {}) {
|
||||
const { showLoading = true } = options;
|
||||
if (showLoading) {
|
||||
uni.showLoading({ title: '绑定中...', mask: true });
|
||||
}
|
||||
try {
|
||||
const code = await getWxLoginCode();
|
||||
const { ok, res } = await bindDoctorWxWechat(code);
|
||||
if (!ok) {
|
||||
throw {
|
||||
msg: (res && (res.message || res.msg)) || '绑定失败,请前往「我的 → 绑定微信」手动绑定',
|
||||
};
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出确认框,用户同意后再绑定微信(内容安全等被动触发场景使用)
|
||||
* @param {Object} options
|
||||
* @param {string} options.title 弹窗标题
|
||||
* @param {string} options.content 弹窗正文
|
||||
* @param {boolean} options.showLoading 绑定过程是否展示 loading
|
||||
* @returns {Promise<boolean>} 绑定成功返回 true,用户取消返回 false
|
||||
*/
|
||||
export function promptDoctorWxBind(options = {}) {
|
||||
const {
|
||||
title = '提示',
|
||||
content = '内容安全校验需要绑定微信,是否立即绑定?',
|
||||
showLoading = true,
|
||||
} = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
title,
|
||||
content,
|
||||
confirmText: '去绑定',
|
||||
cancelText: '取消',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ensureDoctorWxBound({ showLoading });
|
||||
resolve(true);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
},
|
||||
fail: () => resolve(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -72,6 +72,7 @@
|
||||
* @example <u-upload :action="action" :file-list="fileList" ></u-upload>
|
||||
*/
|
||||
import { prepareImagePath } from '@/common/js/image-compress.js';
|
||||
import { checkImage, handleSecurityError, SCENE_PROFILE } from '@/common/js/content-security.js';
|
||||
|
||||
export default {
|
||||
name: 'd-upload',
|
||||
@@ -415,6 +416,15 @@
|
||||
}
|
||||
this.lists[index].error = false;
|
||||
this.uploading = true;
|
||||
try {
|
||||
await checkImage(this.lists[index].url, SCENE_PROFILE);
|
||||
} catch (e) {
|
||||
this.uploading = false;
|
||||
uni.hideLoading();
|
||||
handleSecurityError(e);
|
||||
this.lists[index].error = true;
|
||||
return this.uploadFile(index + 1);
|
||||
}
|
||||
// 创建上传对象
|
||||
const task = uni.uploadFile({
|
||||
url: this.action,
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDoctorWxAuthConfig, getDoctorWxBindStatus, bindDoctorWxWechat } from '@/api/doctorAuth.js';
|
||||
import { getDoctorWxAuthConfig, getDoctorWxBindStatus } from '@/api/doctorAuth.js';
|
||||
import { getCurrentLoginContext } from '@/utils/loginSession.js';
|
||||
import { ensureDoctorWxBound } from '@/common/js/wx-bind-helper.js';
|
||||
|
||||
export default {
|
||||
name: 'WxBindEntry',
|
||||
@@ -75,37 +76,13 @@ export default {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.loading = true;
|
||||
uni.showLoading({ title: '绑定中...', mask: true });
|
||||
new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (loginRes) => {
|
||||
if (!loginRes.code) {
|
||||
reject(new Error('no_code'));
|
||||
return;
|
||||
}
|
||||
resolve(loginRes.code);
|
||||
},
|
||||
fail: () => reject(new Error('login_fail')),
|
||||
});
|
||||
})
|
||||
.then((code) => bindDoctorWxWechat(code))
|
||||
.then(({ data, ok, res }) => {
|
||||
if (ok) {
|
||||
this.bound = true;
|
||||
this.openidMasked = (data && data.openid_masked) || '';
|
||||
ensureDoctorWxBound({ showLoading: false })
|
||||
.then(() => this.refresh())
|
||||
.then(() => {
|
||||
this.$toast && this.$toast('绑定成功');
|
||||
} else {
|
||||
this.$toast && this.$toast((res && (res.message || res.msg)) || '绑定失败');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err && err.message === 'no_code') {
|
||||
this.$toast && this.$toast('获取微信授权失败');
|
||||
} else if (err && err.message === 'login_fail') {
|
||||
this.$toast && this.$toast('微信授权失败');
|
||||
} else {
|
||||
this.$toast && this.$toast('绑定失败');
|
||||
}
|
||||
this.$toast && this.$toast((err && err.msg) || '绑定失败');
|
||||
})
|
||||
.finally(() => {
|
||||
uni.hideLoading();
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { formatMoney } from './format.js'
|
||||
|
||||
/** 展示用:优先顶层合并医嘱(含系统规则文案),fallback content 纯医嘱 */
|
||||
export function formatDoctorOrder(item) {
|
||||
const pres = item && item.prescription ? item.prescription : item
|
||||
if (!pres) return '-'
|
||||
const raw = pres.doctor_order != null && pres.doctor_order !== ''
|
||||
? pres.doctor_order
|
||||
: (pres.content && pres.content.doctor_order) || ''
|
||||
if (Array.isArray(raw)) {
|
||||
const list = raw.map(s => String(s).trim()).filter(Boolean)
|
||||
return list.length ? list.join(';') : '-'
|
||||
}
|
||||
const parts = String(raw).split('|').map(s => s.trim()).filter(Boolean)
|
||||
return parts.length ? parts.join(';') : (raw ? String(raw) : '-')
|
||||
}
|
||||
|
||||
const DEFAULT_DRUG_IMAGE =
|
||||
'https://xkyp-web2.obs.cn-south-1.myhuaweicloud.com/buy/public/drug_no_mage.png'
|
||||
|
||||
@@ -271,7 +286,7 @@ export function mapOrderPrescription(info) {
|
||||
return {
|
||||
prescriptionNo: content.prescription_no || '-',
|
||||
diagnose: content.clinical_diagnose || '-',
|
||||
doctorOrder: content.doctor_order || '-',
|
||||
doctorOrder: formatDoctorOrder(pres),
|
||||
doctorName: doctor.name || '-',
|
||||
departName: depart.name || '-',
|
||||
titleName: title.name || '-',
|
||||
@@ -370,7 +385,7 @@ export function mapPrescriptionDetailView(item) {
|
||||
patientAge: patient.age != null ? String(patient.age) : '-',
|
||||
category: content.category || '-',
|
||||
diagnose: content.clinical_diagnose || '-',
|
||||
doctorOrder: content.doctor_order || '-',
|
||||
doctorOrder: formatDoctorOrder(item),
|
||||
doctorName: doctor.name || '-',
|
||||
departName: depart.name || '-',
|
||||
titleName: title.name || '-',
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { formatMoney } from './format.js'
|
||||
|
||||
/** 展示用:优先顶层合并医嘱(含系统规则文案),fallback content 纯医嘱 */
|
||||
export function formatDoctorOrder(item) {
|
||||
const pres = item && item.prescription ? item.prescription : item
|
||||
if (!pres) return '-'
|
||||
const raw = pres.doctor_order != null && pres.doctor_order !== ''
|
||||
? pres.doctor_order
|
||||
: (pres.content && pres.content.doctor_order) || ''
|
||||
if (Array.isArray(raw)) {
|
||||
const list = raw.map(s => String(s).trim()).filter(Boolean)
|
||||
return list.length ? list.join(';') : '-'
|
||||
}
|
||||
const parts = String(raw).split('|').map(s => s.trim()).filter(Boolean)
|
||||
return parts.length ? parts.join(';') : (raw ? String(raw) : '-')
|
||||
}
|
||||
|
||||
const DEFAULT_DRUG_IMAGE =
|
||||
'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250510/cd470ebf97e31c4048ba502ba71b66a3.jpg'
|
||||
|
||||
@@ -271,7 +286,7 @@ export function mapOrderPrescription(info) {
|
||||
return {
|
||||
prescriptionNo: content.prescription_no || '-',
|
||||
diagnose: content.clinical_diagnose || '-',
|
||||
doctorOrder: content.doctor_order || '-',
|
||||
doctorOrder: formatDoctorOrder(pres),
|
||||
doctorName: doctor.name || '-',
|
||||
departName: depart.name || '-',
|
||||
titleName: title.name || '-',
|
||||
@@ -370,7 +385,7 @@ export function mapPrescriptionDetailView(item) {
|
||||
patientAge: patient.age != null ? String(patient.age) : '-',
|
||||
category: content.category || '-',
|
||||
diagnose: content.clinical_diagnose || '-',
|
||||
doctorOrder: content.doctor_order || '-',
|
||||
doctorOrder: formatDoctorOrder(item),
|
||||
doctorName: doctor.name || '-',
|
||||
departName: depart.name || '-',
|
||||
titleName: title.name || '-',
|
||||
|
||||
@@ -106,7 +106,8 @@
|
||||
<transfer-mode-modal v-model="showTransferModeModal" @select="onTransferModeSelect" />
|
||||
<transfer-patient-drawer
|
||||
ref="photoTransferDrawer"
|
||||
v-model="showPhotoTransferDrawer"
|
||||
:value="showPhotoTransferDrawer"
|
||||
@input="onPhotoTransferDrawerInput"
|
||||
:photo-mode="true"
|
||||
:submitting="photoSubmitting"
|
||||
@confirm="handlePhotoTransferConfirm"
|
||||
@@ -212,24 +213,49 @@ export default {
|
||||
/** 选择传方方式 */
|
||||
onTransferModeSelect(mode) {
|
||||
if (mode === 'photo') {
|
||||
// 先重置再打开,避免跨分包组件 input 事件未解包时父级状态卡在 true
|
||||
this.showPhotoTransferDrawer = false
|
||||
this.$nextTick(() => {
|
||||
this.showPhotoTransferDrawer = true
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/subPackages/sub_workbench/prescription_v2/index?salesperson_transfer=1&initial_category=1',
|
||||
})
|
||||
},
|
||||
/** 拍照传方:抽屉确认后直接提交 */
|
||||
async handlePhotoTransferConfirm(patientData) {
|
||||
this.photoSubmitting = true
|
||||
try {
|
||||
const wrap = await createClinicSalespersonTransferPrescription({
|
||||
...patientData,
|
||||
/** 解包跨分包原生组件 emit 的参数(微信会包在 detail.__args__ 里) */
|
||||
unwrapComponentEventPayload(payload) {
|
||||
if (payload?.detail?.__args__) return payload.detail.__args__[0]
|
||||
if (Array.isArray(payload?.__args__)) return payload.__args__[0]
|
||||
return payload
|
||||
},
|
||||
/** 拍照传方抽屉显隐同步(替代 v-model,避免原生事件包装导致状态不同步) */
|
||||
onPhotoTransferDrawerInput(val) {
|
||||
this.showPhotoTransferDrawer = !!this.unwrapComponentEventPayload(val)
|
||||
},
|
||||
/** 构造拍照传方提交参数(显式字段,避免事件对象被误展开) */
|
||||
buildPhotoTransferPayload(patientData) {
|
||||
const data = this.unwrapComponentEventPayload(patientData) || {}
|
||||
return {
|
||||
patient_name: data.patient_name,
|
||||
patient_mobile: data.patient_mobile,
|
||||
patient_address: data.patient_address,
|
||||
prescription_images: data.prescription_images || [],
|
||||
remark: data.remark || '',
|
||||
transfer_mode: 2,
|
||||
drugs: [],
|
||||
clinical_diagnose: '',
|
||||
doctor_order: '',
|
||||
})
|
||||
}
|
||||
},
|
||||
/** 拍照传方:抽屉确认后直接提交 */
|
||||
async handlePhotoTransferConfirm(patientData) {
|
||||
this.photoSubmitting = true
|
||||
try {
|
||||
const wrap = await createClinicSalespersonTransferPrescription(
|
||||
this.buildPhotoTransferPayload(patientData)
|
||||
)
|
||||
if (wrap && wrap.ok) {
|
||||
this.showPhotoTransferDrawer = false
|
||||
if (this.$refs.photoTransferDrawer && this.$refs.photoTransferDrawer.resetForm) {
|
||||
|
||||
@@ -329,6 +329,7 @@ import ReceptionActionBar from '@/subPackages/sub_online_reception/components/Re
|
||||
import RefuseReceptionModal from '@/subPackages/sub_online_reception/components/RefuseReceptionModal.vue';
|
||||
import { withWxKey, ensureWxKey } from '@/utils/wxListKey.js';
|
||||
import { prepareImagePath } from '@/common/js/image-compress.js';
|
||||
import { checkText, checkImage, handleSecurityError, SCENE_CHAT } from '@/common/js/content-security.js';
|
||||
|
||||
const PENDING_RX_FROM_CHAT = 'xk_pending_rx_from_chat';
|
||||
|
||||
@@ -1007,9 +1008,16 @@ export default {
|
||||
toggleVoiceInput() {
|
||||
this.voiceInputActive = !this.voiceInputActive;
|
||||
},
|
||||
sendTextMessage() {
|
||||
async sendTextMessage() {
|
||||
if (!this.newMessage.trim() || !this.roomId || this.inputLocked) return;
|
||||
this.sendMessage({ type: 'text', content: this.newMessage });
|
||||
const content = this.newMessage;
|
||||
try {
|
||||
await checkText(content, SCENE_CHAT);
|
||||
} catch (e) {
|
||||
handleSecurityError(e);
|
||||
return;
|
||||
}
|
||||
this.sendMessage({ type: 'text', content });
|
||||
this.newMessage = '';
|
||||
this.showToolPanel = false;
|
||||
},
|
||||
@@ -1101,6 +1109,12 @@ export default {
|
||||
size: fileSize,
|
||||
});
|
||||
if (!prepared) return;
|
||||
try {
|
||||
await checkImage(prepared.path, SCENE_CHAT);
|
||||
} catch (e) {
|
||||
handleSecurityError(e);
|
||||
return;
|
||||
}
|
||||
this.uploadMedia(prepared.path, 'image');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
toUploaded
|
||||
} from "@/api/all.js";
|
||||
import { chooseAvatarImage } from '@/common/js/select-avatar-image.js';
|
||||
import { checkProfileTexts, handleSecurityError, SCENE_PROFILE } from '@/common/js/content-security.js';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -231,6 +232,8 @@
|
||||
title_id: "",
|
||||
depart_id: "",
|
||||
work_avator: "",
|
||||
// 内容安全 baseline:详情加载完成后快照
|
||||
_securityBaseline: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -352,6 +355,10 @@
|
||||
if (res.errcode == 0) {
|
||||
// console.log(res.data, 'wd');
|
||||
this.docInfo = res.data;
|
||||
this._securityBaseline = {
|
||||
good_at: (res.data && res.data.good_at) || '',
|
||||
intro: (res.data && res.data.intro) || '',
|
||||
};
|
||||
this.$nextTick(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
@@ -368,6 +375,10 @@
|
||||
if (res.errcode == 0) {
|
||||
// console.log(res, 'wd');
|
||||
this.doctorInfo = res.data;
|
||||
this._securityBaseline = {
|
||||
good_at: (res.data.DoctorInfo && res.data.DoctorInfo.good_at) || '',
|
||||
intro: (res.data.DoctorInfo && res.data.DoctorInfo.intro) || '',
|
||||
};
|
||||
this.$nextTick(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
@@ -425,7 +436,16 @@
|
||||
});
|
||||
},
|
||||
// 保存编辑
|
||||
edit() {
|
||||
async edit() {
|
||||
try {
|
||||
await checkProfileTexts({
|
||||
good_at: this.doctorInfo.DoctorInfo.good_at,
|
||||
intro: this.doctorInfo.DoctorInfo.intro,
|
||||
}, this._securityBaseline, SCENE_PROFILE);
|
||||
} catch (e) {
|
||||
handleSecurityError(e);
|
||||
return;
|
||||
}
|
||||
infoEdit({
|
||||
title_id: this.title_id,
|
||||
yard_id: this.yard_id,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { prepareImagePath } from '@/common/js/image-compress.js'
|
||||
import { uploadToOss } from '@/common/js/oss-upload.js'
|
||||
import { checkImage } from '@/common/js/content-security.js'
|
||||
import { requestGalleryPick } from '@/subPackages/sub_salesperson/common/imageGalleryBridge.js'
|
||||
|
||||
function basenameFromPath(filePath) {
|
||||
@@ -52,6 +53,7 @@ async function uploadChosenImages(res, count = 1) {
|
||||
size: fileMeta.size,
|
||||
})
|
||||
if (!prepared) continue
|
||||
await checkImage(prepared.path, 1)
|
||||
const fileSize = fileMeta.size || 0
|
||||
const fileName = fileMeta.name || basenameFromPath(path)
|
||||
const result = await uploadToOss({ filePath: prepared.path, fileSize, fileName })
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
<view class="row"><text class="k">手机号</text><text class="v">{{ detail.phone || '-' }}</text></view>
|
||||
<view class="row"><text class="k">录入诊所</text><text class="v">{{ detail.clinic_input_count || 0 }} 家</text></view>
|
||||
<view class="row"><text class="k">录入药店</text><text class="v">{{ detail.pharmacy_input_count || 0 }} 家</text></view>
|
||||
<view class="row"><text class="k">正式诊所</text><text class="v">{{ detail.clinic_store_count || 0 }} 家</text></view>
|
||||
<view class="row"><text class="k">正式药店</text><text class="v">{{ detail.pharmacy_store_count || 0 }} 家</text></view>
|
||||
</view>
|
||||
<view v-else class="loading">暂无数据</view>
|
||||
</drawer-page-container>
|
||||
|
||||
@@ -105,7 +105,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
form: {
|
||||
patient_name: '',
|
||||
patient_mobile: '',
|
||||
@@ -134,20 +133,16 @@ export default {
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
computed: {
|
||||
/** 与父级 v-model 双向绑定,避免 data+watch 双状态不同步 */
|
||||
show: {
|
||||
get() { return this.value; },
|
||||
set(v) { this.$emit('input', v); },
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.show = false;
|
||||
this.$emit('input', false);
|
||||
},
|
||||
chooseImage() {
|
||||
uni.chooseImage({
|
||||
|
||||
@@ -2081,7 +2081,10 @@ export default {
|
||||
} else {
|
||||
this.diagnoses = [];
|
||||
}
|
||||
this.medicalAdvice = detail.doctor_order ?? content.doctor_order ?? '';
|
||||
const fallbackOrder = Array.isArray(detail.doctor_order)
|
||||
? detail.doctor_order.join('|')
|
||||
: (detail.doctor_order ?? '');
|
||||
this.medicalAdvice = content.doctor_order ?? fallbackOrder ?? '';
|
||||
this.saveToLocalStorage();
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
import selfIntroduction from './components/self-introduction.vue';
|
||||
import openServe from './components/open-serve.vue';
|
||||
import practiceInfo from './components/practice-info.vue';
|
||||
import { checkProfileTexts, handleSecurityError, SCENE_PROFILE } from '@/common/js/content-security.js';
|
||||
export default {
|
||||
components: {
|
||||
basicInformation,
|
||||
@@ -148,6 +149,8 @@
|
||||
identity: uni.getStorageSync('role') || 1,
|
||||
role: uni.getStorageSync('role') || 1,
|
||||
identitys: "",
|
||||
// 内容安全 baseline:表单初始值快照
|
||||
_securityBaseline: {},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -204,6 +207,11 @@
|
||||
// positioned: { name: userInfo.titles.name },
|
||||
id_card: userInfo.idcard
|
||||
})
|
||||
this._securityBaseline = {
|
||||
name: userInfo.name || '',
|
||||
beGoodAt: userInfo.good_at || '',
|
||||
about: userInfo.intro || '',
|
||||
}
|
||||
uni.setStorageSync('d_name', userInfo.depart)
|
||||
}
|
||||
// 门店
|
||||
@@ -474,6 +482,22 @@
|
||||
// 完善基本信息
|
||||
async stepOne() {
|
||||
console.log(this.info, 'this.infossssssssssssssss');
|
||||
try {
|
||||
if (this.role == 1) {
|
||||
await checkProfileTexts({
|
||||
beGoodAt: this.info[0]['beGoodAt'],
|
||||
about: this.info[0]['about'],
|
||||
name: this.info[0]['name'],
|
||||
}, this._securityBaseline, SCENE_PROFILE);
|
||||
} else if (this.role == 2) {
|
||||
await checkProfileTexts({
|
||||
name: this.info[0]['name'],
|
||||
}, this._securityBaseline, SCENE_PROFILE);
|
||||
}
|
||||
} catch (e) {
|
||||
handleSecurityError(e);
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
name: this.info[0]['name'],
|
||||
avatar: this.info[0]['avatar'],
|
||||
|
||||
@@ -6,8 +6,8 @@ export function checkDev(key = '') {
|
||||
// 判断某些模块是否开启
|
||||
switch (key) {
|
||||
case 'dev':
|
||||
return false;
|
||||
// return true;
|
||||
// return false;
|
||||
return true;
|
||||
case 'open-im':
|
||||
// return false;
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user