This commit is contained in:
左哥 2025-08-06 23:28:08 +08:00
parent b3d6e68356
commit fd695a221e
7 changed files with 313 additions and 91 deletions

View File

@ -1,3 +1,4 @@
import { getOcrReact } from './ocrReact'
export const getOcr = (url) => {
return new Promise((resolve, reject) => {
wx.request({
@ -28,10 +29,23 @@ export const getOcr = (url) => {
}]
},
success(res) {
if(res.data.choices[0].message.content.includes('rotate_rect')){
getOcrReact(url).then(res=>{
resolve(res)
}).catch(err=>{
wx.showToast({
title: '识别失败,请重新选择照片!',
icon: 'none',
duration: 5000
});
reject(err)
})
return
}
let data = parseJsonBlock(res.data.choices[0].message.content)
if(data.length == 0){
wx.showToast({
title: '识别失败,请选择带有符合表单字段的照片!',
title: '识别失败,请重新选择照片!',
icon: 'none',
duration: 5000
});
@ -39,26 +53,26 @@ export const getOcr = (url) => {
return
}
// 判断是否为 rotate_rect 格式
if (Array.isArray(data) && data.length && data[0].rotate_rect) {
let items = extractCheckItemsFromOcrTable(data);
if(items.length==0){
wx.showToast({
title: '识别失败,请选择带有符合表单字段的照片!',
icon: 'none',
duration: 5000
});
reject('识别失败!')
return
}
resolve(items);
return;
}
// if (Array.isArray(data) && data.length && data[0].rotate_rect) {
// let items = extractCheckItemsFromOcrTable(data);
// if(items.length==0){
// wx.showToast({
// title: '识别失败,请重新选择照片!',
// icon: 'none',
// duration: 5000
// });
// reject('识别失败!')
// return
// }
// resolve(items);
// return;
// }
resolve(data);
},
fail(err) {
wx.showToast({
title: '识别失败,请选择带有符合表单字段的照片!',
title: '识别失败,请重新选择照片!',
icon: 'none',
duration: 5000
})

View File

@ -1,4 +1,4 @@
export const getOcr = (url) => {
export const getOcrReact = (url) => {
return new Promise((resolve, reject) => {
wx.request({
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
@ -29,6 +29,7 @@ export const getOcr = (url) => {
},
success(res) {
let data = parseOcrResult(res.data.choices[0].message.content)
console.log(data)
// 新增:统一字段名
if (Array.isArray(data)) {
data = data.map(item => {
@ -115,21 +116,27 @@ function parseMarkdownTable(md) {
// 拆分行,去掉空行
const lines = md.split('\n').filter(line => line.trim().length > 0);
// 找到表头和数据行
const headerLine = lines[0];
// 保留空单元格
const header = headerLine.split('|').map(h => h.trim());
// 检查第二行是否为分隔符(全是 ---
const isSeparator = line => line.split('|').every(cell => cell.trim() === '' || /^-+$/.test(cell.trim()));
// 数据行从第三行开始(第二行为分隔符)
const dataLines = lines.slice(2);
let header = [];
let dataLines = [];
if (lines.length > 1 && isSeparator(lines[1])) {
// 没有表头,第一行是数据
const colCount = lines[0].split('|').length;
header = Array.from({ length: colCount }, (_, i) => `col${i + 1}`);
dataLines = [lines[0], ...lines.slice(2)];
} else {
// 有表头
header = lines[0].split('|').map(h => h.trim());
dataLines = lines.slice(2);
}
// 解析每一行
const result = dataLines.map(line => {
// 保留空单元格
const cells = line.split('|').map(cell => cell.trim());
const obj = {};
header.forEach((key, idx) => {
// 这里 key 可能是空字符串,需跳过
if (key) obj[key] = cells[idx] || '';
});
return obj;

193
api/ocrReact.js Normal file
View File

@ -0,0 +1,193 @@
export const getOcrReact = (url) => {
return new Promise((resolve, reject) => {
wx.request({
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
method: 'POST',
dataType: 'json', // 微信官方文档中介绍会对数据进行一次JSON.parse
header: {
'Authorization': 'Bearer sk-52414b887aee47e4883caf16cbf801bd',
'Content-Type': 'application/json'
},
data: {
"model": "qwen-vl-ocr-latest",
"messages": [{
"role": "user",
"content": [{
"type": "image_url",
"image_url": {
"url": url
},
"min_pixels": 3136,
"max_pixels": 6422528
},
{
"type": "text",
"text": "要求准确无误的提取上述关键信息、不要遗漏和捏造虚假信息,模糊或者强光遮挡的单个文字可以用英文问号?代替。返回数据格式以MD方式输出"
}
]
}]
},
success(res) {
let data = parseOcrResult(res.data.choices[0].message.content)
console.log(data)
// 新增:统一字段名
if (Array.isArray(data)) {
// 找到包含"项目"和"结果"的字段名
let projectField = null;
let resultField = null;
// 检查第一条数据的所有字段
if (data.length > 0) {
Object.keys(data[0]).forEach(key => {
if (typeof data[0][key] === 'string' && data[0][key].includes('项目')) {
projectField = key;
}
if (typeof data[0][key] === 'string' && data[0][key].includes('结果')) {
resultField = key;
}
});
}
// 重新映射所有数据
data = data.map(item => {
const newItem = {};
if (projectField && item[projectField]) {
newItem.name = item[projectField];
}
if (resultField && item[resultField]) {
newItem.value = item[resultField];
}
return newItem;
});
}
resolve(data);
},
fail(err) {
console.log(err)
// 断网、服务器挂了都会fail回调直接reject即可
reject(err);
},
});
})
}
function parseMarkdownTable(md) {
// 拆分行,去掉空行
const lines = md.split('\n').filter(line => line.trim().length > 0);
// 检查第二行是否为分隔符(全是 ---
const isSeparator = line => line.split('|').every(cell => cell.trim() === '' || /^-+$/.test(cell.trim()));
let header = [];
let dataLines = [];
if (lines.length > 1 && isSeparator(lines[1])) {
// 没有表头,第一行是数据
const colCount = lines[0].split('|').length;
header = Array.from({ length: colCount }, (_, i) => `col${i + 1}`);
dataLines = [lines[0], ...lines.slice(2)];
} else {
// 有表头
header = lines[0].split('|').map(h => h.trim());
dataLines = lines.slice(2);
}
// 解析每一行
const result = dataLines.map(line => {
const cells = line.split('|').map(cell => cell.trim());
const obj = {};
header.forEach((key, idx) => {
if (key) obj[key] = cells[idx] || '';
});
return obj;
});
return result;
}
/**
* 解析类似 ```json ... ``` 格式的字符串提取检测项目数组
* @param {string} str
* @returns {Array<Object>}
*/
function parseJsonBlock(str) {
// 去除包裹的代码块标记
const jsonStr = str.replace(/^[\s`]*```json[\s`]*|```$/g, '').replace(/↵/g, '\n').trim();
// 用正则提取所有 "key": "value"
const regex = /"([^"]+)":\s*"([^"]*)"/g;
const pairs = [];
let match;
while ((match = regex.exec(jsonStr)) !== null) {
pairs.push([match[1], match[2]]);
}
// 按“序号”分组
const items = [];
let current = {};
const itemFields = ['序号', '项目名称', '缩写', '结果', '单位', '参考区间', '测定方法'];
pairs.forEach(([key, value]) => {
if (key === '序号' && Object.keys(current).length > 0) {
items.push({ ...current });
current = {};
}
if (itemFields.includes(key)) {
current[key] = value;
}
});
if (Object.keys(current).length > 0) {
items.push({ ...current });
}
return items;
}
/**
* 自动判断OCR返回内容格式并调用对应解析方法
* @param {string} content
* @returns {Array<Object>}
*/
function parseOcrResult(content) {
// 判断是否为JSON代码块
if (/^```json/.test(content.trim())) {
return parseJsonBlock(content);
}
// 判断是否为Markdown表格以|开头,且有---分隔行)
if (/\|.*\|/.test(content) && /\|[\s\-:|]+\|/.test(content)) {
return parseMarkdownTable(content);
}
// 判断是否为实验室结果格式(数字+中文+数字+单位+参考区间)
if (/^\d+[\u4e00-\u9fa5A-Za-z]+[\d.]+[a-zA-Zμ\/]+[\d.\-]+/m.test(content.replace(/↵/g, '\n'))) {
return parseLabResults(content);
}
// 其它情况返回空数组或原始内容
return [];
}
/**
* 解析实验室结果字符串为结构化对象数组
* @param {string} str - 原始字符串
* @returns {Array} 结构化结果数组
*/
function parseLabResults(str) {
if (!str) return [];
// 替换特殊换行符为标准换行
str = str.replace(/↵/g, '\n');
const lines = str.split(/\n+/).filter(Boolean);
const result = [];
const regex = /^(\d+)([\u4e00-\u9fa5A-Za-z]+)([\d.]+)([a-zA-Zμ\/]+)?([\d.\-]+)?/;
lines.forEach(line => {
// 尝试用正则提取
const match = line.match(/^(\d+)([\u4e00-\u9fa5A-Za-z]+)([\d.]+)([a-zA-Zμ\/]+)?([\d.\-]+)?/);
if (match) {
result.push({
index: Number(match[1]),
name: match[2],
value: Number(match[3]),
unit: match[4] || '',
reference: match[5] || ''
});
}
});
return result;
}

View File

@ -3,6 +3,7 @@ import uploadFileToOSS from '~/api/upload';
import { getOcr } from '~/api/ocr';
import { getBOcr } from '~/api/BOcr';
import { getOcrReact } from '~/api/ocrReact';
Page({
/**
@ -262,16 +263,16 @@ Page({
this.setData({
[`form.${mode}`]: arr
})
if(mode == 'mdt_image'){
if (mode == 'mdt_image') {
wx.showToast({ title: '上传成功!', icon: 'none' });
} else if(mode == 'b_mode_image'){
} else if (mode == 'b_mode_image') {
wx.showToast({ title: '上传成功,正在识别内容!', icon: 'none' });
getBOcr(imageUrl).then(ocrRes => {
console.log(ocrRes)
wx.showToast({ title: '识别完成!', icon: 'none' })
this.setFormData(ocrRes, mode)
})
} else{
} else {
wx.showToast({ title: '上传成功,正在识别内容!', icon: 'none' });
getOcr(imageUrl).then(ocrRes => {
console.log(ocrRes)
@ -353,24 +354,25 @@ Page({
// 遍历识别结果,填充字段
setFormData(ocrs, mode) {
ocrs.forEach(ocr => {
const name = ocr.name ? ocr.name.replace(/.*?|\(.*?\)/g, '').trim() : '';
// 血常规
if (mode == 'blood_routine_image') {
if (ocr.name == '血红蛋白' || ocr.name == '血红蛋白检查') {
if (name == '血红蛋白' || name == '血红蛋白检查') {
this.setData({
[`form.hemoglobin`]: ocr.value
})
}
if (ocr.name == '血小板' || ocr.name == '血小板计数') {
if (name == '血小板' || name == '血小板计数') {
this.setData({
[`form.platelets`]: ocr.value
})
}
if (ocr.name == '白细胞' || ocr.name == '白细胞计数') {
if (name == '白细胞' || name == '白细胞计数') {
this.setData({
[`form.white_blood_cells`]: ocr.value
})
}
if (ocr.name == '红细胞' || ocr.name == '红细胞计数') {
if (name == '红细胞' || name == '红细胞计数') {
this.setData({
[`form.red_blood_cells`]: ocr.value
})
@ -378,52 +380,52 @@ Page({
}
// 凝血功能
if (mode == 'coagulation_function_image') {
if (ocr.name == 'C-反应蛋白' || ocr.name == 'c-反应蛋白' || ocr.name == 'CRP') {
if (name == 'C-反应蛋白' || name == 'c-反应蛋白' || name == 'CRP') {
this.setData({
[`form.crp`]: ocr.value
})
}
if (ocr.name == 'DDR' || ocr.name == 'ddr') {
if (name == 'DDR' || name == 'ddr') {
this.setData({
[`form.ddr`]: ocr.value
})
}
if (ocr.name == '凝血酶原时间' || ocr.name == 'Pt') {
if (name == '凝血酶原时间' || name == 'Pt') {
this.setData({
[`form.pt`]: ocr.value
})
}
if (ocr.name == '凝血酶原活动度' || ocr.name == 'Pt-a' || ocr.name == 'PtA') {
if (name == '凝血酶原活动度' || name == 'Pt-a' || name == 'PtA') {
this.setData({
[`form.pta`]: ocr.value
})
}
if (ocr.name == '国际标准化比值' || ocr.name == 'INR') {
if (name == '国际标准化比值' || name == 'INR' || name == 'INR值' || name == 'PT国际标准化比值') {
this.setData({
[`form.inr`]: ocr.value
})
}
if (ocr.name == '活化部分凝血活酶时间' || ocr.name == 'APTT') {
if (name == '活化部分凝血活酶时间' || name == 'APTT') {
this.setData({
[`form.aptt`]: ocr.value
})
}
if (ocr.name == '凝血酶时间' || ocr.name == 'TT') {
if (name == '凝血酶时间' || name == 'TT') {
this.setData({
[`form.tt`]: ocr.value
})
}
if (ocr.name == '纤维蛋白原' || ocr.name == 'FIB') {
if (name == '纤维蛋白原' || name == 'FIB') {
this.setData({
[`form.fib`]: ocr.value
})
}
if (ocr.name == 'NPDP' || ocr.name == 'npdp') {
if (name == 'NPDP' || name == 'npdp') {
this.setData({
[`form.npdp`]: ocr.value
})
}
if (ocr.name == 'MMP-7' || ocr.name == 'MMP7' || ocr.name == '基质金属蛋白酶-7') {
if (name == 'MMP-7' || name == 'MMP7' || name == '基质金属蛋白酶-7') {
this.setData({
[`form.mmp_7`]: ocr.value
})
@ -432,43 +434,43 @@ Page({
// 肝功能
if (mode == 'liver_function_image') {
if (ocr.name == '总胆红素') {
if (name == '总胆红素') {
this.setData({
[`form.total_bilirubin`]: ocr.value
})
}
if (ocr.name == '直接胆红素') {
if (name == '直接胆红素') {
this.setData({
[`form.direct_bilirubin`]: ocr.value
})
}
if (ocr.name == '总胆汁酸') {
if (name == '总胆汁酸') {
this.setData({
[`form.total_bile_acid`]: ocr.value
})
}
if (ocr.name == '白蛋白') {
if (name == '白蛋白') {
this.setData({
[`form.albumin`]: ocr.value
})
}
if (ocr.name == '谷草' || ocr.name == '谷草转氨酶') {
if (name == '谷草' || name == '谷草转氨酶') {
this.setData({
[`form.grain_grass`]: ocr.value
})
}
if (ocr.name == '谷丙' || ocr.name == '谷丙转氨酶') {
if (name == '谷丙' || name == '谷丙转氨酶') {
this.setData({
[`form.gu_bing`]: ocr.value
})
}
if (ocr.name == 'γ-谷氨酰转肽酶' || ocr.name == '谷氨酰转肽酶') {
if (name == 'γ-谷氨酰转肽酶' || name == '谷氨酰转肽酶') {
this.setData({
[`form.ggt`]: ocr.value
})
}
if (ocr.name == '碱性磷酸酶') {
if (name == '碱性磷酸酶') {
this.setData({
[`form.alp`]: ocr.value
})
@ -476,32 +478,32 @@ Page({
}
// 营养指数
if (mode == 'nutritional_indicator_image') {
if (ocr.name == '25-羟基维生素D3' || ocr.name == '25-OH-Vitamin D3' || ocr.name == '25(OH)D3') {
if (name == '25-羟基维生素D3' || name == '25-OH-Vitamin D3' || name == '25(OH)D3') {
this.setData({
[`form.oh_d3`]: ocr.value
})
}
if (ocr.name == '25-羟基维生素D2' || ocr.name == '25-OH-Vitamin D2' || ocr.name == '25(OH)D2') {
if (name == '25-羟基维生素D2' || name == '25-OH-Vitamin D2' || name == '25(OH)D2') {
this.setData({
[`form.oh_d2`]: ocr.value
})
}
if (ocr.name == '25-羟基维生素D' || ocr.name == '25-OH-Vitamin D' || ocr.name == '25(OH)D') {
if (name == '25-羟基维生素D' || name == '25-OH-Vitamin D' || name == '25(OH)D') {
this.setData({
[`form.oh_d`]: ocr.value
})
}
if (ocr.name == '维生素A' || ocr.name == '维生素a' || ocr.name == 'vitamin a' || ocr.name == 'vit A') {
if (name == '维生素A' || name == '维生素a' || name == 'vitamin a' || name == 'vit A') {
this.setData({
[`form.vitamin_a`]: ocr.value
})
}
if (ocr.name == '维生素K' || ocr.name == '维生素k' || ocr.name == 'vitamin k' || ocr.name == 'vit K') {
if (name == '维生素K' || name == '维生素k' || name == 'vitamin k' || name == 'vit K') {
this.setData({
[`form.vitamin_k`]: ocr.value
})
}
if (ocr.name == '维生素E' || ocr.name == '维生素e' || ocr.name == 'vitamin e' || ocr.name == 'vit E') {
if (name == '维生素E' || name == '维生素e' || name == 'vitamin e' || name == 'vit E') {
this.setData({
[`form.vitamin_e`]: ocr.value
})
@ -509,77 +511,77 @@ Page({
}
// B超
if (mode == 'b_mode_image') {
if(ocr.name == '肝肋下'){
if (name == '肝肋下') {
this.setData({
[`form.under_the_liver_rib`]: ocr.value
})
}
if(ocr.name == '肝剑突下'){
if (name == '肝剑突下') {
this.setData({
[`form.under_the_xiphoid_liver`]: ocr.value
})
}
if(ocr.name == '脾肋下'){
if (name == '脾肋下') {
this.setData({
[`form.spleen_rib_area`]: ocr.value
})
}
if(ocr.name == '门静脉主干内径'){
if (name == '门静脉主干内径') {
this.setData({
[`form.main_portal_vein`]: ocr.value
})
}
if(ocr.name == '肝回声'){
if (name == '肝回声') {
this.setData({
[`form.liver_echo`]: ocr.value
})
}
if(ocr.name == '胆囊大小'){
if (name == '胆囊大小') {
this.setData({
[`form.gallbladder_size`]: ocr.value
})
}
if(ocr.name == '胆总管'){
if (name == '胆总管') {
this.setData({
[`form.common_bile_duct`]: ocr.value
})
}
if(ocr.name == '纤维块大小'){
if (name == '纤维块大小') {
this.setData({
[`form.fiber_block_size`]: ocr.value
})
}
if(ocr.name == '门静脉流速' || ocr.name == 'PVV' || ocr.name == 'PVV流速' || ocr.name == 'pvv'){
if (name == '门静脉流速' || name == 'PVV' || name == 'PVV流速' || name == 'pvv') {
this.setData({
[`form.pvv`]: ocr.value
})
}
if(ocr.name == '肝弹性值'){
if (name == '肝弹性值') {
this.setData({
[`form.liver_elasticity_value`]: ocr.value
})
}
if(ocr.name == '有无肝囊肿'){
if (name == '有无肝囊肿') {
this.setData({
[`form.is_have_cyst`]: ocr.value == '无' ? '2' : '1'
})
}
if(ocr.name == '有无腹水'){
if (name == '有无腹水') {
this.setData({
[`form.is_have_ascites`]: ocr.value == '无' ? '2' : '1'
})
}
if(ocr.name == '弹性成像最小值'){
if (name == '弹性成像最小值') {
this.setData({
[`form.elastography_minimum`]: ocr.value
})
}
if(ocr.name == '弹性成像最大值'){
if (name == '弹性成像最大值') {
this.setData({
[`form.elastography_maximum`]: ocr.value
})
}
if(ocr.name == '弹性成像中位数'){
if (name == '弹性成像中位数') {
this.setData({
[`form.elastography_median`]: ocr.value
})

View File

@ -325,10 +325,11 @@
position: absolute;
left: 24rpx;
top: 408rpx;
width: 702rpx;
width: calc(100% - 48rpx);
right: 24rpx;
// height: 210rpx;
display: flex;
flex-direction: column;
// display: flex;
// flex-direction: column;
}
.block_5 {
background-color: rgba(255,255,255,0.010000);
@ -338,10 +339,10 @@
padding: 38rpx 32rpx 32rpx 32rpx;
}
.image-text_5 {
width: 192rpx;
flex-direction: row;
display: flex;
justify-content: space-between;
// width: 192rpx;
// flex-direction: row;
// display: flex;
// justify-content: space-between;
}
.thumbnail_2 {
width: 40rpx;
@ -387,14 +388,14 @@
z-index: 0;
}
.image-text_6 {
width: 192rpx;
flex-direction: row;
display: flex;
justify-content: space-between;
// width: 192rpx;
}
.thumbnail_4 {
width: 40rpx;
height: 40rpx;
vertical-align: bottom;
margin-right: 20rpx;
}
.text-group_7 {
overflow-wrap: break-word;

View File

@ -63,10 +63,10 @@
<text class="iconfont icon-youjiantou label_4"></text>
</view>
<view class="section_3">
<view class="block_5" data-url="/pages/therapeuticRegimen/index" bind:tap="toPath">
<view class="image-text_5">
<image src="https://image-fudan.oss-cn-beijing.aliyuncs.com/mini_images/my/yyfa.svg" class="thumbnail_2"></image>
<text lines="1" class="text-group_6">用药方案</text>
<view class="block_7" data-url="/pages/therapeuticRegimen/index" bind:tap="toPath">
<view class="image-text_6">
<image src="https://image-fudan.oss-cn-beijing.aliyuncs.com/mini_images/my/yyfa.svg" class="thumbnail_4"></image>
<text lines="1" class="text-group_7">用药方案</text>
</view>
<text class="iconfont icon-youjiantou thumbnail_3"></text>
</view>
@ -81,7 +81,7 @@
<view class="block_7" data-url="/pages/mmp-7/index" bind:tap="toPath">
<view class="image-text_6">
<image src="https://image-fudan.oss-cn-beijing.aliyuncs.com/mini_images/my/jkjy.svg" class="thumbnail_4"></image>
<text lines="1" class="text-group_7">MMP-7</text>
<text class="text-group_7">MMP-7</text>
</view>
<text class="iconfont icon-youjiantou thumbnail_3"></text>
</view>

View File

@ -165,7 +165,8 @@ Page({
avatar: info.avatar ? info.avatar : 'https://image-fudan.oss-cn-beijing.aliyuncs.com/mini_images/my/baby.png',
}
})
if (wx.getStorageSync('scrm_userid')) {
if (wx.getStorageSync('scrm_userid') && info.operative_date) {
this.scrmBindTag(wx.getStorageSync('scrm_userid'))
}
},
@ -476,6 +477,9 @@ Page({
obj.gestational_week = Number(obj.gestational_week);
await request('patient/set_personal_information', 'post', obj);
if (wx.getStorageSync('scrm_userid')) {
this.scrmBindTag(wx.getStorageSync('scrm_userid'))
}
wx.showToast({
title: '提交成功',
icon: 'success',
@ -486,6 +490,7 @@ Page({
})
}
})
},
onIdInput(e) {
// 18位身份证正则