| @@ -2,7 +2,7 @@ | |||||
| CREATE TABLE `message` ( | CREATE TABLE `message` ( | ||||
| `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, | ||||
| `patient_id` INT UNSIGNED NOT NULL COMMENT '患者ID', | `patient_id` INT UNSIGNED NOT NULL COMMENT '患者ID', | ||||
| `type` TINYINT NOT NULL DEFAULT 0 COMMENT '类型:1=审核通过 2=审核拒绝 3=回寄信息', | |||||
| `type` TINYINT NOT NULL DEFAULT 0 COMMENT '类型:1=审核通过 2=审核拒绝 3=回寄信息 4=送检修改申请审核结果', | |||||
| `title` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '标题', | `title` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '标题', | ||||
| `content` TEXT COMMENT '详细内容', | `content` TEXT COMMENT '详细内容', | ||||
| `reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '驳回原因', | `reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '驳回原因', | ||||
| @@ -0,0 +1,24 @@ | |||||
| -- 送检信息修改申请与送检流转日志 | |||||
| ALTER TABLE `patient` | |||||
| MODIFY COLUMN `sample_info_status` tinyint(1) DEFAULT 0 COMMENT '送检信息状态: 0可修改 1已生效 2已寄回 3修改申请待审核', | |||||
| ADD COLUMN `sample_edit_reason` varchar(500) DEFAULT '' COMMENT '送检信息修改申请原因' AFTER `return_time`, | |||||
| ADD COLUMN `sample_edit_reject_reason` varchar(500) DEFAULT '' COMMENT '送检信息修改申请驳回原因' AFTER `sample_edit_reason`, | |||||
| ADD COLUMN `sample_edit_apply_time` datetime DEFAULT NULL COMMENT '送检信息修改申请时间' AFTER `sample_edit_reject_reason`, | |||||
| ADD COLUMN `sample_edit_audit_time` datetime DEFAULT NULL COMMENT '送检信息修改申请审核时间' AFTER `sample_edit_apply_time`; | |||||
| CREATE TABLE IF NOT EXISTS `patient_sample_log` ( | |||||
| `id` int unsigned NOT NULL AUTO_INCREMENT, | |||||
| `patient_id` int unsigned NOT NULL COMMENT '患者ID', | |||||
| `action` varchar(50) NOT NULL DEFAULT '' COMMENT '操作类型', | |||||
| `title` varchar(100) NOT NULL DEFAULT '' COMMENT '日志标题', | |||||
| `content` text COMMENT '日志内容', | |||||
| `reason` varchar(500) NOT NULL DEFAULT '' COMMENT '原因', | |||||
| `operator_type` varchar(20) NOT NULL DEFAULT '' COMMENT '操作人类型: patient/admin/system', | |||||
| `operator_id` int unsigned NOT NULL DEFAULT 0 COMMENT '操作人ID', | |||||
| `operator_name` varchar(100) NOT NULL DEFAULT '' COMMENT '操作人名称', | |||||
| `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | |||||
| PRIMARY KEY (`id`), | |||||
| KEY `idx_patient_id` (`patient_id`), | |||||
| KEY `idx_action` (`action`), | |||||
| KEY `idx_create_time` (`create_time`) | |||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='患者送检流转日志'; | |||||
| @@ -12,4 +12,6 @@ ALTER TABLE `wechat_user` | |||||
| ADD COLUMN `mp_env_version` varchar(20) DEFAULT 'release' COMMENT '小程序版本: develop/trial/release' AFTER `patient_id`; | ADD COLUMN `mp_env_version` varchar(20) DEFAULT 'release' COMMENT '小程序版本: develop/trial/release' AFTER `patient_id`; | ||||
| -- 更新 pap_mini_cytx 应用的模板配置 | -- 更新 pap_mini_cytx 应用的模板配置 | ||||
| UPDATE `sys_wechat_app` SET `subscribe_templates` = '{"audit_result":"OcMLIGS8pMnpGzZmPkdMEOmmWmvvvU_nN-kmiK5sUGE"}' WHERE `remark` = 'pap_mini_cytx'; | |||||
| UPDATE `sys_wechat_app` | |||||
| SET `subscribe_templates` = '{"audit_result":"OcMLIGS8pMnpGzZmPkdMEOmmWmvvvU_nN-kmiK5sUGE","sample_edit_audit":"OcMLIGS8pMnpGzZmPkdMELDgFefqOeQ-1jsit8Ldw2w"}' | |||||
| WHERE `remark` = 'pap_mini_cytx'; | |||||
| @@ -54,6 +54,9 @@ module.exports = [ | |||||
| ['/admin/patient/saveSampleReceiverConfig', 'admin/patient/saveSampleReceiverConfig', 'post'], | ['/admin/patient/saveSampleReceiverConfig', 'admin/patient/saveSampleReceiverConfig', 'post'], | ||||
| ['/admin/patient/resetSampleInfo', 'admin/patient/resetSampleInfo', 'post'], | ['/admin/patient/resetSampleInfo', 'admin/patient/resetSampleInfo', 'post'], | ||||
| ['/admin/patient/saveReturnTrackingNo', 'admin/patient/saveReturnTrackingNo', 'post'], | ['/admin/patient/saveReturnTrackingNo', 'admin/patient/saveReturnTrackingNo', 'post'], | ||||
| ['/admin/patient/approveSampleEdit', 'admin/patient/approveSampleEdit', 'post'], | |||||
| ['/admin/patient/rejectSampleEdit', 'admin/patient/rejectSampleEdit', 'post'], | |||||
| ['/admin/patient/editSampleInfo', 'admin/patient/editSampleInfo', 'post'], | |||||
| ['/admin/patient/export', 'admin/patient/export'], | ['/admin/patient/export', 'admin/patient/export'], | ||||
| // 公共接口 | // 公共接口 | ||||
| ['/common/regions', 'common/regions'], | ['/common/regions', 'common/regions'], | ||||
| @@ -83,6 +86,7 @@ module.exports = [ | |||||
| ['/api/mp/subscribeConfig', 'mp/subscribeConfig'], | ['/api/mp/subscribeConfig', 'mp/subscribeConfig'], | ||||
| ['/api/mp/sampleInfo', 'mp/sampleInfo'], | ['/api/mp/sampleInfo', 'mp/sampleInfo'], | ||||
| ['/api/mp/saveSampleInfo', 'mp/saveSampleInfo', 'post'], | ['/api/mp/saveSampleInfo', 'mp/saveSampleInfo', 'post'], | ||||
| ['/api/mp/applySampleInfoEdit', 'mp/applySampleInfoEdit', 'post'], | |||||
| ['/api/mp/regenerateSign', 'mp/regenerateSign'], | ['/api/mp/regenerateSign', 'mp/regenerateSign'], | ||||
| ['/api/mp/regenerateSignByUrl', 'mp/regenerateSignByUrl', 'post'], | ['/api/mp/regenerateSignByUrl', 'mp/regenerateSignByUrl', 'post'], | ||||
| @@ -1,13 +1,119 @@ | |||||
| const Base = require('../base.js'); | const Base = require('../base.js'); | ||||
| const dayjs = require('dayjs'); | const dayjs = require('dayjs'); | ||||
| const APP_REMARK = 'pap_mini_cytx'; | |||||
| const AUDIT_APPROVED_CONTENT = '您提交的肠愈同行患者关爱项目资料已审核通过,请于7天内在小程序-个人中心-送检信息中提交相关信息。'; | const AUDIT_APPROVED_CONTENT = '您提交的肠愈同行患者关爱项目资料已审核通过,请于7天内在小程序-个人中心-送检信息中提交相关信息。'; | ||||
| const SAMPLE_EDIT_AUDIT_TEMPLATE_ID = 'OcMLIGS8pMnpGzZmPkdMELDgFefqOeQ-1jsit8Ldw2w'; | |||||
| module.exports = class extends Base { | module.exports = class extends Base { | ||||
| _canEditPatient() { | _canEditPatient() { | ||||
| return this.isSuperAdmin || (this.userPermissions || []).includes('patient:edit'); | return this.isSuperAdmin || (this.userPermissions || []).includes('patient:edit'); | ||||
| } | } | ||||
| _adminOperator() { | |||||
| const user = this.adminUser || {}; | |||||
| return { | |||||
| operator_id: user.id || 0, | |||||
| operator_name: user.nickname || user.username || '' | |||||
| }; | |||||
| } | |||||
| _parseArrayValue(value) { | |||||
| if (Array.isArray(value)) return value; | |||||
| if (!value) return []; | |||||
| try { return JSON.parse(value || '[]') || []; } catch (e) { return []; } | |||||
| } | |||||
| _hasSampleInfo(patient = {}) { | |||||
| const sampleTypes = this._parseArrayValue(patient.sample_types); | |||||
| const samplePhotos = this._parseArrayValue(patient.sample_photos); | |||||
| return !!( | |||||
| sampleTypes.length || | |||||
| samplePhotos.length || | |||||
| patient.report_email || | |||||
| patient.sample_tracking_no || | |||||
| patient.return_name || | |||||
| patient.return_phone || | |||||
| patient.return_address | |||||
| ); | |||||
| } | |||||
| _sampleInfoStatusText(patient = {}) { | |||||
| const status = Number(patient.sample_info_status) || 0; | |||||
| if (status === 3) return '修改申请待审核'; | |||||
| if (status === 2) return '已寄回'; | |||||
| if (status === 1) return '已生效'; | |||||
| return this._hasSampleInfo(patient) ? '可修改' : '未提交'; | |||||
| } | |||||
| _sampleInfoStatusType(patient = {}) { | |||||
| const status = Number(patient.sample_info_status) || 0; | |||||
| if (status === 3) return 'warning'; | |||||
| if (status === 2) return 'primary'; | |||||
| if (status === 1) return 'success'; | |||||
| return this._hasSampleInfo(patient) ? 'warning' : 'info'; | |||||
| } | |||||
| _buildFlowLogs(audits = [], sampleLogs = []) { | |||||
| const auditLogs = audits.map(item => { | |||||
| const action = item.action || ''; | |||||
| let title = '资料流转'; | |||||
| let content = ''; | |||||
| if (action === 'submit') { | |||||
| title = '提交患者资料'; | |||||
| content = `${item.operator_name || '系统'} 提交了患者资料,等待审核`; | |||||
| } else if (action === 'approve') { | |||||
| title = '患者资料审核通过'; | |||||
| content = `${item.operator_name || '系统'} 审核通过`; | |||||
| } else if (action === 'reject') { | |||||
| title = '患者资料审核驳回'; | |||||
| content = `${item.operator_name || '系统'} 驳回了资料`; | |||||
| } | |||||
| return { | |||||
| id: item.id, | |||||
| source: 'audit', | |||||
| action, | |||||
| title, | |||||
| content, | |||||
| reason: item.reason || '', | |||||
| operator_name: item.operator_name || '', | |||||
| operator_type: 'admin', | |||||
| create_time: item.create_time | |||||
| }; | |||||
| }); | |||||
| const logs = sampleLogs.map(item => ({ | |||||
| id: item.id, | |||||
| source: 'sample', | |||||
| action: item.action || '', | |||||
| title: item.title || '送检信息', | |||||
| content: item.content || '', | |||||
| reason: item.reason || '', | |||||
| operator_name: item.operator_name || '', | |||||
| operator_type: item.operator_type || '', | |||||
| create_time: item.create_time | |||||
| })); | |||||
| return auditLogs.concat(logs).sort((a, b) => { | |||||
| const ta = new Date(a.create_time || 0).getTime(); | |||||
| const tb = new Date(b.create_time || 0).getTime(); | |||||
| if (tb !== ta) return tb - ta; | |||||
| return (b.id || 0) - (a.id || 0); | |||||
| }); | |||||
| } | |||||
| async _addSampleLog(patientId, action, title, content, reason = '') { | |||||
| const operator = this._adminOperator(); | |||||
| return this.model('patient_sample_log').addLog({ | |||||
| patient_id: patientId, | |||||
| action, | |||||
| title, | |||||
| content, | |||||
| reason, | |||||
| operator_type: 'admin', | |||||
| operator_id: operator.operator_id, | |||||
| operator_name: operator.operator_name | |||||
| }); | |||||
| } | |||||
| // 患者列表页面 | // 患者列表页面 | ||||
| async indexAction() { | async indexAction() { | ||||
| this.assign('currentPage', 'patient'); | this.assign('currentPage', 'patient'); | ||||
| @@ -60,6 +166,8 @@ module.exports = class extends Base { | |||||
| const cName = regionMap[item.city_code] || ''; | const cName = regionMap[item.city_code] || ''; | ||||
| const dName = regionMap[item.district_code] || ''; | const dName = regionMap[item.district_code] || ''; | ||||
| item.region_name = [pName, cName, dName].filter(Boolean).join(' '); | item.region_name = [pName, cName, dName].filter(Boolean).join(' '); | ||||
| item.sample_info_status_text = this._sampleInfoStatusText(item); | |||||
| item.sample_info_status_type = this._sampleInfoStatusType(item); | |||||
| }); | }); | ||||
| const counts = await model.getStatusCounts({ keyword, tag, startDate, endDate, province_code, city_code, district_code }); | const counts = await model.getStatusCounts({ keyword, tag, startDate, endDate, province_code, city_code, district_code }); | ||||
| @@ -125,13 +233,21 @@ module.exports = class extends Base { | |||||
| patient.hospital_district_name = regionMap[patient.hospital_district_code] || ''; | patient.hospital_district_name = regionMap[patient.hospital_district_code] || ''; | ||||
| } | } | ||||
| // 获取审核记录 | |||||
| patient.sample_info_status_text = this._sampleInfoStatusText(patient); | |||||
| patient.sample_info_status_type = this._sampleInfoStatusType(patient); | |||||
| // 获取审核记录和送检流转日志 | |||||
| const audits = await this.model('patient_audit') | const audits = await this.model('patient_audit') | ||||
| .where({ patient_id: id }) | .where({ patient_id: id }) | ||||
| .order('id DESC') | .order('id DESC') | ||||
| .select(); | .select(); | ||||
| const sampleLogs = await this.model('patient_sample_log') | |||||
| .where({ patient_id: id }) | |||||
| .order('id DESC') | |||||
| .select(); | |||||
| const flowLogs = this._buildFlowLogs(audits, sampleLogs); | |||||
| return this.success({ patient, audits }); | |||||
| return this.success({ patient, audits: flowLogs, flowLogs }); | |||||
| } | } | ||||
| async sampleReceiverConfigAction() { | async sampleReceiverConfigAction() { | ||||
| @@ -372,9 +488,14 @@ module.exports = class extends Base { | |||||
| await this.model('patient').where({ id }).update({ | await this.model('patient').where({ id }).update({ | ||||
| sample_info_status: 0, | sample_info_status: 0, | ||||
| sample_edit_reason: '', | |||||
| sample_edit_reject_reason: '', | |||||
| sample_edit_apply_time: null, | |||||
| sample_edit_audit_time: null, | |||||
| update_by: this.adminUser?.id || 0 | update_by: this.adminUser?.id || 0 | ||||
| }); | }); | ||||
| await this._addSampleLog(id, 'sample_reset_edit', '重置送检信息编辑', '后台重置送检信息为可修改'); | |||||
| await this.log('edit', '患者管理', `重置送检信息编辑权限(ID:${id})`); | await this.log('edit', '患者管理', `重置送检信息编辑权限(ID:${id})`); | ||||
| return this.success(); | return this.success(); | ||||
| } | } | ||||
| @@ -409,10 +530,119 @@ module.exports = class extends Base { | |||||
| is_read: 0 | is_read: 0 | ||||
| }); | }); | ||||
| await this._addSampleLog(id, 'sample_return', Number(patient.sample_info_status) === 2 ? '修改回寄物流单号' : '填写回寄物流单号', `回寄物流单号:${trackingNo}`); | |||||
| await this.log('edit', '患者管理', `填写回寄物流单号(ID:${id}):${trackingNo}`); | await this.log('edit', '患者管理', `填写回寄物流单号(ID:${id}):${trackingNo}`); | ||||
| return this.success(); | return this.success(); | ||||
| } | } | ||||
| async approveSampleEditAction() { | |||||
| if (!this._canEditPatient()) return this.fail('暂无权限'); | |||||
| const { id } = this.post(); | |||||
| if (!id) return this.fail('参数错误'); | |||||
| const patient = await this.model('patient').where({ id, is_deleted: 0 }).find(); | |||||
| if (think.isEmpty(patient)) return this.fail('患者不存在'); | |||||
| if (Number(patient.sample_info_status) !== 3) return this.fail('当前没有待审核的送检修改申请'); | |||||
| const reason = patient.sample_edit_reason || ''; | |||||
| await this.model('patient').where({ id }).update({ | |||||
| sample_info_status: 0, | |||||
| sample_edit_reason: '', | |||||
| sample_edit_reject_reason: '', | |||||
| sample_edit_apply_time: null, | |||||
| sample_edit_audit_time: null, | |||||
| update_by: this.adminUser?.id || 0 | |||||
| }); | |||||
| await this._addSampleLog(id, 'sample_approve_edit', '送检修改申请通过', '后台通过送检信息修改申请,患者可重新编辑送检信息', reason); | |||||
| await this._addSampleEditMessage(id, true); | |||||
| await this._sendSampleEditSubscribeMessage(id, patient.name, '审核通过', '请重新提交送检信息'); | |||||
| await this.log('edit', '患者管理', `通过送检信息修改申请(ID:${id})`); | |||||
| return this.success(); | |||||
| } | |||||
| async rejectSampleEditAction() { | |||||
| if (!this._canEditPatient()) return this.fail('暂无权限'); | |||||
| const { id, reason } = this.post(); | |||||
| if (!id) return this.fail('参数错误'); | |||||
| if (!reason || !reason.trim()) return this.fail('请填写驳回原因'); | |||||
| const patient = await this.model('patient').where({ id, is_deleted: 0 }).find(); | |||||
| if (think.isEmpty(patient)) return this.fail('患者不存在'); | |||||
| if (Number(patient.sample_info_status) !== 3) return this.fail('当前没有待审核的送检修改申请'); | |||||
| const rejectReason = reason.trim(); | |||||
| const now = think.datetime(new Date()); | |||||
| await this.model('patient').where({ id }).update({ | |||||
| sample_info_status: 1, | |||||
| sample_edit_reject_reason: rejectReason, | |||||
| sample_edit_audit_time: now, | |||||
| update_by: this.adminUser?.id || 0 | |||||
| }); | |||||
| await this._addSampleLog(id, 'sample_reject_edit', '送检修改申请驳回', '后台驳回送检信息修改申请', rejectReason); | |||||
| await this._addSampleEditMessage(id, false, rejectReason); | |||||
| await this._sendSampleEditSubscribeMessage(id, patient.name, '审核拒绝', '修改申请未通过,请查看详情'); | |||||
| await this.log('edit', '患者管理', `驳回送检信息修改申请(ID:${id}),原因:${rejectReason}`); | |||||
| return this.success(); | |||||
| } | |||||
| async editSampleInfoAction() { | |||||
| if (!this._canEditPatient()) return this.fail('暂无权限'); | |||||
| const data = this.post(); | |||||
| const { id, sample_types, wax_return, return_name, return_phone, return_province_code, return_city_code, return_district_code, return_address, report_email, sample_tracking_no, sample_photos } = data; | |||||
| if (!id) return this.fail('参数错误'); | |||||
| const patient = await this.model('patient').where({ id, is_deleted: 0 }).find(); | |||||
| if (think.isEmpty(patient)) return this.fail('患者不存在'); | |||||
| const finalSampleTypes = this._parseArrayValue(sample_types); | |||||
| const finalSamplePhotos = this._parseArrayValue(sample_photos); | |||||
| const hasSample = finalSampleTypes.length > 0; | |||||
| const needReturn = hasSample && Number(wax_return) === 1; | |||||
| const finalReportEmail = hasSample ? (report_email || '').trim() : ''; | |||||
| if (needReturn) { | |||||
| if (!return_name || !return_name.trim()) return this.fail('请填写回寄收件人'); | |||||
| if (!return_phone || !return_phone.trim()) return this.fail('请填写回寄收件电话'); | |||||
| if (!return_province_code || !return_city_code || !return_district_code) return this.fail('请选择回寄收件地址'); | |||||
| if (!return_address || !return_address.trim()) return this.fail('请填写回寄详细地址'); | |||||
| } | |||||
| if (finalReportEmail) { | |||||
| if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(finalReportEmail)) return this.fail('邮箱格式不正确'); | |||||
| const dup = await this.model('patient') | |||||
| .where({ report_email: finalReportEmail, is_deleted: 0, id: ['!=', id] }) | |||||
| .find(); | |||||
| if (!think.isEmpty(dup)) return this.fail('该邮箱已被其他患者使用'); | |||||
| } | |||||
| await this.model('patient').where({ id }).update({ | |||||
| sample_types: JSON.stringify(finalSampleTypes), | |||||
| wax_return: needReturn ? 1 : 0, | |||||
| return_name: needReturn ? (return_name || '').trim() : '', | |||||
| return_phone: needReturn ? (return_phone || '').trim() : '', | |||||
| return_province_code: needReturn ? (return_province_code || '') : '', | |||||
| return_city_code: needReturn ? (return_city_code || '') : '', | |||||
| return_district_code: needReturn ? (return_district_code || '') : '', | |||||
| return_address: needReturn ? (return_address || '').trim() : '', | |||||
| report_email: hasSample ? finalReportEmail : '', | |||||
| sample_tracking_no: hasSample ? (sample_tracking_no || '').trim() : '', | |||||
| sample_photos: hasSample ? JSON.stringify(finalSamplePhotos) : '[]', | |||||
| sample_info_status: 1, | |||||
| return_tracking_no: '', | |||||
| return_time: null, | |||||
| sample_edit_reason: '', | |||||
| sample_edit_reject_reason: '', | |||||
| sample_edit_apply_time: null, | |||||
| sample_edit_audit_time: null, | |||||
| update_by: this.adminUser?.id || 0 | |||||
| }); | |||||
| await this._addSampleLog(id, 'sample_admin_edit', '后台编辑送检信息', '后台直接编辑送检信息,状态已生效'); | |||||
| await this.log('edit', '患者管理', `后台编辑送检信息(ID:${id})`); | |||||
| return this.success(); | |||||
| } | |||||
| // 编辑患者 | // 编辑患者 | ||||
| async editAction() { | async editAction() { | ||||
| const data = this.post(); | const data = this.post(); | ||||
| @@ -617,7 +847,6 @@ module.exports = class extends Base { | |||||
| // @private 发送审核结果订阅消息 | // @private 发送审核结果订阅消息 | ||||
| async _sendAuditSubscribeMessage(patientId, patientName, result) { | async _sendAuditSubscribeMessage(patientId, patientName, result) { | ||||
| try { | try { | ||||
| const APP_REMARK = 'pap_mini_cytx'; | |||||
| // 查找该患者关联的微信用户 | // 查找该患者关联的微信用户 | ||||
| const wechatUser = await this.model('wechat_user') | const wechatUser = await this.model('wechat_user') | ||||
| .where({ patient_id: patientId, app_remark: APP_REMARK, status: 1 }) | .where({ patient_id: patientId, app_remark: APP_REMARK, status: 1 }) | ||||
| @@ -654,6 +883,53 @@ module.exports = class extends Base { | |||||
| } | } | ||||
| } | } | ||||
| async _addSampleEditMessage(patientId, approved, reason = '') { | |||||
| await this.model('message').add({ | |||||
| patient_id: patientId, | |||||
| type: 4, | |||||
| title: approved ? '送检信息修改申请已通过' : '送检信息修改申请未通过', | |||||
| content: approved | |||||
| ? '您的送检信息修改申请已通过,请前往送检信息页面重新提交。' | |||||
| : '您的送检信息修改申请未通过,请查看原因。', | |||||
| reason: approved ? '' : reason, | |||||
| is_read: 0 | |||||
| }); | |||||
| } | |||||
| // @private 发送送检修改申请审核结果订阅消息 | |||||
| async _sendSampleEditSubscribeMessage(patientId, patientName, result, remark) { | |||||
| try { | |||||
| const wechatUser = await this.model('wechat_user') | |||||
| .where({ patient_id: patientId, app_remark: APP_REMARK, status: 1 }) | |||||
| .find(); | |||||
| if (think.isEmpty(wechatUser) || !wechatUser.open_id) return; | |||||
| const wechatService = this.service('wechat'); | |||||
| const templates = await wechatService.getSubscribeTemplates(APP_REMARK); | |||||
| const templateId = templates.sample_edit_audit || SAMPLE_EDIT_AUDIT_TEMPLATE_ID; | |||||
| if (!templateId) return; | |||||
| const envMap = { develop: 'developer', trial: 'trial', release: 'formal' }; | |||||
| const miniprogramState = envMap[wechatUser.mp_env_version] || 'formal'; | |||||
| const limitThing = value => String(value || '').slice(0, 20); | |||||
| await wechatService.sendSubscribeMessage({ | |||||
| remark: APP_REMARK, | |||||
| openid: wechatUser.open_id, | |||||
| templateId, | |||||
| page: 'pages/profile/profile', | |||||
| miniprogramState, | |||||
| data: { | |||||
| thing4: { value: limitThing(remark || '送检信息修改申请已处理') }, | |||||
| phrase1: { value: result }, | |||||
| thing16: { value: limitThing(patientName || '患者') }, | |||||
| time13: { value: dayjs().format('YYYY-MM-DD HH:mm:ss') } | |||||
| } | |||||
| }); | |||||
| } catch (error) { | |||||
| think.logger.error('[Subscribe] 送检修改申请审核消息发送失败:', error); | |||||
| } | |||||
| } | |||||
| // @private 发送审核结果短信通知 | // @private 发送审核结果短信通知 | ||||
| async _sendAuditSms(phone, type, reason) { | async _sendAuditSms(phone, type, reason) { | ||||
| if (!phone) return; | if (!phone) return; | ||||
| @@ -7,8 +7,29 @@ const cosConfig = require('../config/cos.js'); | |||||
| const APP_REMARK = 'pap_mini_cytx'; | const APP_REMARK = 'pap_mini_cytx'; | ||||
| const AUDIT_APPROVED_CONTENT = '您提交的肠愈同行患者关爱项目资料已审核通过,请于7天内在小程序-个人中心-送检信息中提交相关信息。'; | const AUDIT_APPROVED_CONTENT = '您提交的肠愈同行患者关爱项目资料已审核通过,请于7天内在小程序-个人中心-送检信息中提交相关信息。'; | ||||
| const SAMPLE_EDIT_AUDIT_TEMPLATE_ID = 'OcMLIGS8pMnpGzZmPkdMELDgFefqOeQ-1jsit8Ldw2w'; | |||||
| module.exports = class extends Base { | module.exports = class extends Base { | ||||
| _parseArrayValue(value) { | |||||
| if (Array.isArray(value)) return value; | |||||
| if (!value) return []; | |||||
| try { return JSON.parse(value || '[]') || []; } catch (e) { return []; } | |||||
| } | |||||
| _hasSampleInfo(patient = {}) { | |||||
| const sampleTypes = this._parseArrayValue(patient.sample_types); | |||||
| const samplePhotos = this._parseArrayValue(patient.sample_photos); | |||||
| return !!( | |||||
| sampleTypes.length || | |||||
| samplePhotos.length || | |||||
| patient.report_email || | |||||
| patient.sample_tracking_no || | |||||
| patient.return_name || | |||||
| patient.return_phone || | |||||
| patient.return_address | |||||
| ); | |||||
| } | |||||
| // POST /api/mp/login | // POST /api/mp/login | ||||
| async loginAction() { | async loginAction() { | ||||
| const code = this.post('code'); | const code = this.post('code'); | ||||
| @@ -672,10 +693,11 @@ module.exports = class extends Base { | |||||
| try { | try { | ||||
| const wechatService = this.service('wechat'); | const wechatService = this.service('wechat'); | ||||
| const templates = await wechatService.getSubscribeTemplates(APP_REMARK); | const templates = await wechatService.getSubscribeTemplates(APP_REMARK); | ||||
| if (!templates.sample_edit_audit) templates.sample_edit_audit = SAMPLE_EDIT_AUDIT_TEMPLATE_ID; | |||||
| return this.json({ code: 0, data: templates }); | return this.json({ code: 0, data: templates }); | ||||
| } catch (error) { | } catch (error) { | ||||
| think.logger.error('subscribeConfig error:', error); | think.logger.error('subscribeConfig error:', error); | ||||
| return this.json({ code: 0, data: {} }); | |||||
| return this.json({ code: 0, data: { sample_edit_audit: SAMPLE_EDIT_AUDIT_TEMPLATE_ID } }); | |||||
| } | } | ||||
| } | } | ||||
| @@ -729,6 +751,10 @@ module.exports = class extends Base { | |||||
| sample_info_status: Number(patient.sample_info_status) || 0, | sample_info_status: Number(patient.sample_info_status) || 0, | ||||
| return_tracking_no: patient.return_tracking_no || '', | return_tracking_no: patient.return_tracking_no || '', | ||||
| return_time: patient.return_time || '', | return_time: patient.return_time || '', | ||||
| sample_edit_reason: patient.sample_edit_reason || '', | |||||
| sample_edit_reject_reason: patient.sample_edit_reject_reason || '', | |||||
| sample_edit_apply_time: patient.sample_edit_apply_time || '', | |||||
| sample_edit_audit_time: patient.sample_edit_audit_time || '', | |||||
| sample_receiver_info: sampleReceiverInfo | sample_receiver_info: sampleReceiverInfo | ||||
| }}); | }}); | ||||
| } catch (error) { | } catch (error) { | ||||
| @@ -768,16 +794,30 @@ module.exports = class extends Base { | |||||
| } | } | ||||
| if (sampleInfoStatus === 1) { | if (sampleInfoStatus === 1) { | ||||
| const contactPhone = sampleReceiverInfo.contact_phone || ''; | const contactPhone = sampleReceiverInfo.contact_phone || ''; | ||||
| const msg = '送检信息已生效,如需修改请点击页面的【申请修改送检信息】按钮申请,通过后可重新提交送检信息。' + | |||||
| (contactPhone ? `详情咨询${contactPhone}。` : ''); | |||||
| return this.json({ | return this.json({ | ||||
| code: 1010, | code: 1010, | ||||
| data: { sample_info_status: 1, sample_receiver_info: sampleReceiverInfo }, | data: { sample_info_status: 1, sample_receiver_info: sampleReceiverInfo }, | ||||
| msg: contactPhone | |||||
| ? `送检信息已生效,如需修改请联系平台【${contactPhone}】重置修改权限。` | |||||
| : '送检信息已生效,如需修改请联系平台重置修改权限。' | |||||
| msg | |||||
| }); | |||||
| } | |||||
| if (sampleInfoStatus === 3) { | |||||
| return this.json({ | |||||
| code: 1010, | |||||
| data: { | |||||
| sample_info_status: 3, | |||||
| sample_edit_reason: patient.sample_edit_reason || '', | |||||
| sample_edit_apply_time: patient.sample_edit_apply_time || '', | |||||
| sample_receiver_info: sampleReceiverInfo | |||||
| }, | |||||
| msg: '送检信息修改申请正在审核中,请等待平台处理。' | |||||
| }); | }); | ||||
| } | } | ||||
| const hasSample = sample_types && sample_types.length > 0; | const hasSample = sample_types && sample_types.length > 0; | ||||
| const hadSampleInfo = this._hasSampleInfo(patient); | |||||
| const now = think.datetime(new Date()); | |||||
| // 邮箱格式与重复校验 | // 邮箱格式与重复校验 | ||||
| if (hasSample && report_email) { | if (hasSample && report_email) { | ||||
| @@ -802,7 +842,22 @@ module.exports = class extends Base { | |||||
| report_email: hasSample ? (report_email || '') : '', | report_email: hasSample ? (report_email || '') : '', | ||||
| sample_tracking_no: hasSample ? (sample_tracking_no || '') : '', | sample_tracking_no: hasSample ? (sample_tracking_no || '') : '', | ||||
| sample_photos: hasSample ? JSON.stringify(sample_photos || []) : '[]', | sample_photos: hasSample ? JSON.stringify(sample_photos || []) : '[]', | ||||
| sample_info_status: 1 | |||||
| sample_info_status: 1, | |||||
| sample_edit_reason: '', | |||||
| sample_edit_reject_reason: '', | |||||
| sample_edit_apply_time: null, | |||||
| sample_edit_audit_time: null, | |||||
| update_time: now | |||||
| }); | |||||
| await this.model('patient_sample_log').addLog({ | |||||
| patient_id: user.patient_id, | |||||
| action: hadSampleInfo ? 'sample_update' : 'sample_submit', | |||||
| title: hadSampleInfo ? '修改送检信息' : '提交送检信息', | |||||
| content: hadSampleInfo ? '患者修改并提交送检信息' : '患者提交送检信息', | |||||
| operator_type: 'patient', | |||||
| operator_id: user.id || 0, | |||||
| operator_name: patient.name || '患者' | |||||
| }); | }); | ||||
| return this.json({ code: 0, data: {}, msg: '提交成功' }); | return this.json({ code: 0, data: {}, msg: '提交成功' }); | ||||
| @@ -812,6 +867,60 @@ module.exports = class extends Base { | |||||
| } | } | ||||
| } | } | ||||
| /** | |||||
| * 申请修改送检信息 | |||||
| * POST /api/mp/applySampleInfoEdit | |||||
| */ | |||||
| async applySampleInfoEditAction() { | |||||
| const mpUser = this.mpUser; | |||||
| if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); | |||||
| const { reason, mp_env_version } = this.post(); | |||||
| if (!reason || !reason.trim()) return this.json({ code: 1, msg: '请填写申请原因' }); | |||||
| try { | |||||
| const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find(); | |||||
| if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 1, msg: '请先完成实名认证' }); | |||||
| const patient = await this.model('patient').where({ id: user.patient_id, is_deleted: 0 }).find(); | |||||
| if (think.isEmpty(patient)) return this.json({ code: 1, msg: '患者信息不存在' }); | |||||
| if (patient.status !== 1) return this.json({ code: 1, msg: '请先通过审核' }); | |||||
| const sampleInfoStatus = Number(patient.sample_info_status) || 0; | |||||
| if (sampleInfoStatus === 3) return this.json({ code: 1, msg: '修改申请正在审核中,请勿重复提交' }); | |||||
| if (sampleInfoStatus === 2) return this.json({ code: 1, msg: '样品已经寄回,暂不支持申请修改送检信息' }); | |||||
| if (sampleInfoStatus !== 1) return this.json({ code: 1, msg: '当前送检信息无需申请修改' }); | |||||
| const now = think.datetime(new Date()); | |||||
| const applyReason = reason.trim(); | |||||
| if (mp_env_version) { | |||||
| await this.model('wechat_user').where({ id: user.id }).update({ mp_env_version }); | |||||
| } | |||||
| await this.model('patient').where({ id: user.patient_id }).update({ | |||||
| sample_info_status: 3, | |||||
| sample_edit_reason: applyReason, | |||||
| sample_edit_reject_reason: '', | |||||
| sample_edit_apply_time: now, | |||||
| sample_edit_audit_time: null, | |||||
| update_time: now | |||||
| }); | |||||
| await this.model('patient_sample_log').addLog({ | |||||
| patient_id: user.patient_id, | |||||
| action: 'sample_apply_edit', | |||||
| title: '申请修改送检信息', | |||||
| content: '患者申请修改送检信息', | |||||
| reason: applyReason, | |||||
| operator_type: 'patient', | |||||
| operator_id: user.id || 0, | |||||
| operator_name: patient.name || '患者' | |||||
| }); | |||||
| return this.json({ code: 0, data: { sample_info_status: 3 }, msg: '申请已提交,请等待平台审核' }); | |||||
| } catch (error) { | |||||
| think.logger.error('applySampleInfoEdit error:', error); | |||||
| return this.json({ code: 1, msg: '提交失败: ' + error.message }); | |||||
| } | |||||
| } | |||||
| /** | /** | ||||
| * 批量重新生成声明与承诺签署图并更新数据库 | * 批量重新生成声明与承诺签署图并更新数据库 | ||||
| * GET /api/mp/regenerateSign | * GET /api/mp/regenerateSign | ||||
| @@ -0,0 +1,19 @@ | |||||
| module.exports = class extends think.Model { | |||||
| get tableName() { | |||||
| return 'patient_sample_log'; | |||||
| } | |||||
| async addLog(data = {}) { | |||||
| return this.add({ | |||||
| patient_id: data.patient_id || 0, | |||||
| action: data.action || '', | |||||
| title: data.title || '', | |||||
| content: data.content || '', | |||||
| reason: data.reason || '', | |||||
| operator_type: data.operator_type || '', | |||||
| operator_id: data.operator_id || 0, | |||||
| operator_name: data.operator_name || '', | |||||
| create_time: data.create_time || think.datetime(new Date()) | |||||
| }); | |||||
| } | |||||
| }; | |||||
| @@ -20,8 +20,12 @@ | |||||
| .timeline-list li::before { content: ''; position: absolute; left: -6px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: var(--el-color-primary); } | .timeline-list li::before { content: ''; position: absolute; left: -6px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: var(--el-color-primary); } | ||||
| .timeline-list li .time { font-size: 12px; color: #909399; margin-bottom: 4px; } | .timeline-list li .time { font-size: 12px; color: #909399; margin-bottom: 4px; } | ||||
| .timeline-list li .desc { font-size: 14px; color: #303133; } | .timeline-list li .desc { font-size: 14px; color: #303133; } | ||||
| .timeline-list li .subdesc { font-size: 13px; color: #606266; margin-top: 4px; } | |||||
| .timeline-list li .reason { font-size: 13px; color: #F56C6C; margin-top: 4px; } | .timeline-list li .reason { font-size: 13px; color: #F56C6C; margin-top: 4px; } | ||||
| .fixed-bottom-bar { position: fixed; bottom: 0; left: 220px; right: 0; height: 64px; background: #fff; box-shadow: 0 -2px 8px rgba(0,0,0,0.08); display: flex; align-items: center; justify-content: center; gap: 16px; z-index: 10; padding: 0 24px; } | .fixed-bottom-bar { position: fixed; bottom: 0; left: 220px; right: 0; height: 64px; background: #fff; box-shadow: 0 -2px 8px rgba(0,0,0,0.08); display: flex; align-items: center; justify-content: center; gap: 16px; z-index: 10; padding: 0 24px; } | ||||
| .sample-photo-edit { position: relative; width: 80px; height: 80px; } | |||||
| .sample-photo-edit .el-image { width: 80px; height: 80px; border-radius: 6px; border: 1px solid #eee; } | |||||
| .sample-photo-edit .del { position: absolute; top: -6px; right: -6px; cursor: pointer; background: rgba(0,0,0,0.6); color: #fff; border-radius: 50%; width: 20px; height: 20px; display:flex; align-items:center; justify-content:center; font-size:12px; z-index:10; line-height:1; } | |||||
| </style> | </style> | ||||
| {% endblock %} | {% endblock %} | ||||
| @@ -172,6 +176,10 @@ | |||||
| <div class="info-item" v-if="patient.sample_tracking_no"><span class="label">物流单号:</span><span class="value">${ patient.sample_tracking_no }</span></div> | <div class="info-item" v-if="patient.sample_tracking_no"><span class="label">物流单号:</span><span class="value">${ patient.sample_tracking_no }</span></div> | ||||
| <div class="info-item" v-if="patient.sample_info_status == 2 && patient.return_tracking_no"><span class="label">回寄单号:</span><span class="value">${ patient.return_tracking_no }</span></div> | <div class="info-item" v-if="patient.sample_info_status == 2 && patient.return_tracking_no"><span class="label">回寄单号:</span><span class="value">${ patient.return_tracking_no }</span></div> | ||||
| <div class="info-item" v-if="patient.sample_info_status == 2 && patient.return_time"><span class="label">回寄时间:</span><span class="value">${ patient.return_time }</span></div> | <div class="info-item" v-if="patient.sample_info_status == 2 && patient.return_time"><span class="label">回寄时间:</span><span class="value">${ patient.return_time }</span></div> | ||||
| <div class="info-item" v-if="patient.sample_edit_reason"><span class="label">申请原因:</span><span class="value">${ patient.sample_edit_reason }</span></div> | |||||
| <div class="info-item" v-if="patient.sample_edit_apply_time"><span class="label">申请时间:</span><span class="value">${ patient.sample_edit_apply_time }</span></div> | |||||
| <div class="info-item" v-if="patient.sample_edit_reject_reason"><span class="label">驳回原因:</span><span class="value">${ patient.sample_edit_reject_reason }</span></div> | |||||
| <div class="info-item" v-if="patient.sample_edit_audit_time"><span class="label">处理时间:</span><span class="value">${ patient.sample_edit_audit_time }</span></div> | |||||
| </div> | </div> | ||||
| <div v-if="patient.sample_photos && patient.sample_photos.length" style="margin-top:16px;"> | <div v-if="patient.sample_photos && patient.sample_photos.length" style="margin-top:16px;"> | ||||
| <div style="font-size:13px;color:#909399;margin-bottom:8px;">送检单照片</div> | <div style="font-size:13px;color:#909399;margin-bottom:8px;">送检单照片</div> | ||||
| @@ -184,19 +192,18 @@ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <!-- 审核记录 --> | |||||
| <!-- 流转日志 --> | |||||
| <div class="detail-panel"> | <div class="detail-panel"> | ||||
| <h3>审核记录</h3> | |||||
| <h3>流转日志</h3> | |||||
| <ul class="timeline-list" v-if="audits.length"> | <ul class="timeline-list" v-if="audits.length"> | ||||
| <li v-for="(item, idx) in audits" :key="idx"> | <li v-for="(item, idx) in audits" :key="idx"> | ||||
| <div class="time">${ item.create_time }</div> | <div class="time">${ item.create_time }</div> | ||||
| <div class="desc" v-if="item.action === 'submit'">${ item.operator_name || '系统' } 提交了患者资料,等待审核</div> | |||||
| <div class="desc" v-else-if="item.action === 'approve'">${ item.operator_name } 审核通过</div> | |||||
| <div class="desc" v-else-if="item.action === 'reject'">${ item.operator_name } 驳回了资料</div> | |||||
| <div class="reason" v-if="item.reason">驳回原因:${ item.reason }</div> | |||||
| <div class="desc">${ item.title || item.content || '流转记录' }</div> | |||||
| <div class="subdesc" v-if="item.content && item.title">${ item.content }</div> | |||||
| <div class="reason" v-if="item.reason">${ item.action === 'reject' || item.action === 'sample_reject_edit' ? '驳回原因' : '原因' }:${ item.reason }</div> | |||||
| </li> | </li> | ||||
| </ul> | </ul> | ||||
| <el-empty v-else description="暂无审核记录" :image-size="60" /> | |||||
| <el-empty v-else description="暂无流转日志" :image-size="60" /> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -205,6 +212,9 @@ | |||||
| <el-button @click="goBack">返 回</el-button> | <el-button @click="goBack">返 回</el-button> | ||||
| <el-button v-if="(patient.status === 0 || patient.status === 2) && canAudit" type="success" @click="handleApprove">审核通过</el-button> | <el-button v-if="(patient.status === 0 || patient.status === 2) && canAudit" type="success" @click="handleApprove">审核通过</el-button> | ||||
| <el-button v-if="(patient.status === 0 || patient.status === 1) && canAudit" type="danger" @click="showRejectDialog">驳 回</el-button> | <el-button v-if="(patient.status === 0 || patient.status === 1) && canAudit" type="danger" @click="showRejectDialog">驳 回</el-button> | ||||
| <el-button v-if="canEdit" type="primary" @click="showSampleEditDialog">编辑送检信息</el-button> | |||||
| <el-button v-if="patient.sample_info_status == 3 && canEdit" type="success" @click="approveSampleEdit">通过送检修改申请</el-button> | |||||
| <el-button v-if="patient.sample_info_status == 3 && canEdit" type="danger" @click="showRejectSampleEditDialog">驳回送检修改申请</el-button> | |||||
| <el-button v-if="patient.sample_info_status == 1 && canEdit" type="warning" @click="resetSampleInfo">重置送检信息编辑</el-button> | <el-button v-if="patient.sample_info_status == 1 && canEdit" type="warning" @click="resetSampleInfo">重置送检信息编辑</el-button> | ||||
| <el-button v-if="patient.wax_return && (patient.sample_info_status == 1 || patient.sample_info_status == 2) && canEdit" type="primary" @click="showReturnTrackingDialog"> | <el-button v-if="patient.wax_return && (patient.sample_info_status == 1 || patient.sample_info_status == 2) && canEdit" type="primary" @click="showReturnTrackingDialog"> | ||||
| ${ patient.sample_info_status == 2 ? '修改回寄物流单号' : '填写回寄物流单号' } | ${ patient.sample_info_status == 2 ? '修改回寄物流单号' : '填写回寄物流单号' } | ||||
| @@ -239,6 +249,75 @@ | |||||
| </div> | </div> | ||||
| </el-dialog> | </el-dialog> | ||||
| <!-- 驳回送检修改申请弹窗 --> | |||||
| <el-dialog v-model="sampleRejectVisible" title="驳回送检修改申请" width="520px" destroy-on-close :close-on-click-modal="false" :z-index="2000"> | |||||
| <el-form label-width="90px"> | |||||
| <el-form-item label="驳回原因" required> | |||||
| <el-input v-model="sampleRejectReason" type="textarea" :rows="4" placeholder="请输入驳回原因" maxlength="500" show-word-limit></el-input> | |||||
| </el-form-item> | |||||
| </el-form> | |||||
| <div style="text-align:right;margin-top:20px;"> | |||||
| <el-button @click="sampleRejectVisible = false">取消</el-button> | |||||
| <el-button type="danger" @click="rejectSampleEdit" :loading="sampleRejectSaving">确认驳回</el-button> | |||||
| </div> | |||||
| </el-dialog> | |||||
| <!-- 编辑送检信息弹窗 --> | |||||
| <el-dialog v-model="sampleEditVisible" title="编辑送检信息" width="760px" destroy-on-close draggable :close-on-click-modal="false" :z-index="2000"> | |||||
| <el-form :model="sampleEditForm" label-width="120px"> | |||||
| <el-form-item label="送检样本类型"> | |||||
| <el-checkbox-group v-model="sampleEditForm.sample_types" @change="onSampleEditTypesChange"> | |||||
| <el-checkbox v-for="st in sampleTypeList" :key="st.id" :label="st.name">${ st.name }</el-checkbox> | |||||
| </el-checkbox-group> | |||||
| </el-form-item> | |||||
| <el-form-item v-if="sampleEditShowWaxReturn" :label="sampleEditNeedReturnNames + '是否需寄回'"> | |||||
| <el-radio-group v-model="sampleEditForm.wax_return"> | |||||
| <el-radio :label="1">是</el-radio> | |||||
| <el-radio :label="0">否</el-radio> | |||||
| </el-radio-group> | |||||
| </el-form-item> | |||||
| <template v-if="sampleEditShowWaxReturn && sampleEditForm.wax_return === 1"> | |||||
| <el-form-item label="回寄收件人" required> | |||||
| <el-input v-model="sampleEditForm.return_name" placeholder="请输入回寄收件人"></el-input> | |||||
| </el-form-item> | |||||
| <el-form-item label="回寄电话" required> | |||||
| <el-input v-model="sampleEditForm.return_phone" placeholder="请输入回寄电话" maxlength="20"></el-input> | |||||
| </el-form-item> | |||||
| <el-form-item label="回寄地区" required> | |||||
| <el-cascader v-model="sampleEditForm.returnRegionCodes" :options="regionTree" | |||||
| :props="{ value: 'code', label: 'name', children: 'children' }" | |||||
| placeholder="请选择省/市/区" clearable style="width:100%;" /> | |||||
| </el-form-item> | |||||
| <el-form-item label="详细地址" required> | |||||
| <el-input v-model="sampleEditForm.return_address" placeholder="请输入详细地址"></el-input> | |||||
| </el-form-item> | |||||
| </template> | |||||
| <template v-if="sampleEditForm.sample_types && sampleEditForm.sample_types.length"> | |||||
| <el-form-item label="报告接收邮箱"> | |||||
| <el-input v-model="sampleEditForm.report_email" placeholder="请输入邮箱地址"></el-input> | |||||
| </el-form-item> | |||||
| <el-form-item label="送检物流单号"> | |||||
| <el-input v-model="sampleEditForm.sample_tracking_no" placeholder="请输入送检物流单号"></el-input> | |||||
| </el-form-item> | |||||
| <el-form-item label="送检单照片"> | |||||
| <div style="display:flex;flex-wrap:wrap;gap:10px;"> | |||||
| <div class="sample-photo-edit" v-for="(photo, idx) in sampleEditForm.sample_photos" :key="idx"> | |||||
| <el-image :src="photo" fit="cover" :preview-src-list="sampleEditForm.sample_photos" :initial-index="idx"></el-image> | |||||
| <div class="del" @click="sampleEditForm.sample_photos.splice(idx, 1)">×</div> | |||||
| </div> | |||||
| <el-upload action="/admin/upload" :show-file-list="false" accept="image/*" :headers="uploadHeaders" :on-success="onSampleEditPhotoUpload"> | |||||
| <div style="width:80px;height:80px;border:1px dashed #DCDFE6;border-radius:6px;display:flex;align-items:center;justify-content:center;color:#909399;font-size:24px;cursor:pointer;">+</div> | |||||
| </el-upload> | |||||
| </div> | |||||
| </el-form-item> | |||||
| </template> | |||||
| </el-form> | |||||
| <div style="text-align:right;margin-top:20px;"> | |||||
| <el-button @click="sampleEditVisible = false">取消</el-button> | |||||
| <el-button type="primary" @click="saveSampleEdit" :loading="sampleEditSaving">保存并生效</el-button> | |||||
| </div> | |||||
| </el-dialog> | |||||
| </div>{% endblock %} | </div>{% endblock %} | ||||
| {% block js %} | {% block js %} | ||||
| @@ -269,6 +348,8 @@ var app = createApp({ | |||||
| return_address: '', | return_address: '', | ||||
| report_email: '', sample_tracking_no: '', sample_photos: [], | report_email: '', sample_tracking_no: '', sample_photos: [], | ||||
| sample_info_status: 0, return_tracking_no: '', return_time: '', | sample_info_status: 0, return_tracking_no: '', return_time: '', | ||||
| sample_edit_reason: '', sample_edit_reject_reason: '', | |||||
| sample_edit_apply_time: '', sample_edit_audit_time: '', | |||||
| sign_income: '', sign_privacy: '', sign_promise: '', | sign_income: '', sign_privacy: '', sign_promise: '', | ||||
| emergency_contact: '', emergency_phone: '', | emergency_contact: '', emergency_phone: '', | ||||
| status: -1, create_time: '' | status: -1, create_time: '' | ||||
| @@ -280,7 +361,27 @@ var app = createApp({ | |||||
| var returnTrackingVisible = ref(false); | var returnTrackingVisible = ref(false); | ||||
| var returnTrackingSaving = ref(false); | var returnTrackingSaving = ref(false); | ||||
| var returnTrackingNo = ref(''); | var returnTrackingNo = ref(''); | ||||
| var selectedReasons = ref([]); var commonReasons = [ | |||||
| var uploadHeaders = {}; | |||||
| var sampleRejectVisible = ref(false); | |||||
| var sampleRejectSaving = ref(false); | |||||
| var sampleRejectReason = ref(''); | |||||
| var sampleEditVisible = ref(false); | |||||
| var sampleEditSaving = ref(false); | |||||
| var sampleTypeList = ref([]); | |||||
| var regionTree = ref([]); | |||||
| var sampleEditForm = reactive({ | |||||
| sample_types: [], | |||||
| wax_return: 0, | |||||
| return_name: '', | |||||
| return_phone: '', | |||||
| returnRegionCodes: [], | |||||
| return_address: '', | |||||
| report_email: '', | |||||
| sample_tracking_no: '', | |||||
| sample_photos: [] | |||||
| }); | |||||
| var selectedReasons = ref([]); | |||||
| var commonReasons = [ | |||||
| '身份证照片模糊,请重新上传', | '身份证照片模糊,请重新上传', | ||||
| '病历资料不完整,请补充', | '病历资料不完整,请补充', | ||||
| '检查报告缺失,请上传', | '检查报告缺失,请上传', | ||||
| @@ -305,6 +406,20 @@ var app = createApp({ | |||||
| } | } | ||||
| } | } | ||||
| async function loadSampleTypes() { | |||||
| try { | |||||
| var res = await fetch('/common/sampleTypes').then(function(r) { return r.json(); }); | |||||
| if (res.code === 0) sampleTypeList.value = (res.data && res.data.list) || []; | |||||
| } catch(e) {} | |||||
| } | |||||
| async function loadRegions() { | |||||
| try { | |||||
| var res = await fetch('/common/regions').then(function(r) { return r.json(); }); | |||||
| if (res.code === 0) regionTree.value = res.data || []; | |||||
| } catch(e) {} | |||||
| } | |||||
| function goBack() { | function goBack() { | ||||
| window.location.href = '/admin/patient.html'; | window.location.href = '/admin/patient.html'; | ||||
| } | } | ||||
| @@ -349,13 +464,31 @@ var app = createApp({ | |||||
| return [patient.id_card_front, patient.id_card_back, patient.photo].filter(Boolean); | return [patient.id_card_front, patient.id_card_back, patient.photo].filter(Boolean); | ||||
| }); | }); | ||||
| var sampleEditShowWaxReturn = Vue.computed(function() { | |||||
| if (!sampleEditForm.sample_types || !sampleEditForm.sample_types.length) return false; | |||||
| return sampleEditForm.sample_types.some(function(name) { | |||||
| var st = sampleTypeList.value.find(function(item) { return item.name === name; }); | |||||
| return st && st.need_return; | |||||
| }); | |||||
| }); | |||||
| var sampleEditNeedReturnNames = Vue.computed(function() { | |||||
| if (!sampleEditForm.sample_types || !sampleEditForm.sample_types.length) return ''; | |||||
| return sampleEditForm.sample_types.filter(function(name) { | |||||
| var st = sampleTypeList.value.find(function(item) { return item.name === name; }); | |||||
| return st && st.need_return; | |||||
| }).join('、'); | |||||
| }); | |||||
| function sampleInfoStatusText(status) { | function sampleInfoStatusText(status) { | ||||
| if (Number(status) === 3) return '修改申请待审核'; | |||||
| if (Number(status) === 2) return '已寄回'; | if (Number(status) === 2) return '已寄回'; | ||||
| if (Number(status) === 1) return '已生效'; | if (Number(status) === 1) return '已生效'; | ||||
| return '可修改'; | return '可修改'; | ||||
| } | } | ||||
| function sampleInfoStatusType(status) { | function sampleInfoStatusType(status) { | ||||
| if (Number(status) === 3) return 'warning'; | |||||
| if (Number(status) === 2) return 'primary'; | if (Number(status) === 2) return 'primary'; | ||||
| if (Number(status) === 1) return 'success'; | if (Number(status) === 1) return 'success'; | ||||
| return 'warning'; | return 'warning'; | ||||
| @@ -466,16 +599,155 @@ var app = createApp({ | |||||
| } | } | ||||
| } | } | ||||
| onMounted(function() { loadDetail(); }); | |||||
| async function approveSampleEdit() { | |||||
| try { | |||||
| await ElementPlus.ElMessageBox.confirm( | |||||
| '确认通过该患者的送检信息修改申请吗?通过后患者可在小程序重新编辑并提交送检信息。', | |||||
| '通过送检修改申请', | |||||
| { confirmButtonText: '确认通过', cancelButtonText: '取消', type: 'success' } | |||||
| ); | |||||
| var res = await fetch('/admin/patient/approveSampleEdit', { | |||||
| method: 'POST', | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify({ id: patient.id }) | |||||
| }).then(function(r) { return r.json(); }); | |||||
| if (res.code === 0) { | |||||
| ElementPlus.ElMessage.success('已通过申请'); | |||||
| loadDetail(); | |||||
| } else { | |||||
| ElementPlus.ElMessage.error(res.msg || '操作失败'); | |||||
| } | |||||
| } catch(e) {} | |||||
| } | |||||
| function showRejectSampleEditDialog() { | |||||
| sampleRejectReason.value = ''; | |||||
| sampleRejectVisible.value = true; | |||||
| } | |||||
| async function rejectSampleEdit() { | |||||
| if (!sampleRejectReason.value.trim()) { | |||||
| ElementPlus.ElMessage.warning('请填写驳回原因'); | |||||
| return; | |||||
| } | |||||
| sampleRejectSaving.value = true; | |||||
| try { | |||||
| var res = await fetch('/admin/patient/rejectSampleEdit', { | |||||
| method: 'POST', | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify({ id: patient.id, reason: sampleRejectReason.value.trim() }) | |||||
| }).then(function(r) { return r.json(); }); | |||||
| if (res.code === 0) { | |||||
| ElementPlus.ElMessage.success('已驳回申请'); | |||||
| sampleRejectVisible.value = false; | |||||
| loadDetail(); | |||||
| } else { | |||||
| ElementPlus.ElMessage.error(res.msg || '操作失败'); | |||||
| } | |||||
| } finally { | |||||
| sampleRejectSaving.value = false; | |||||
| } | |||||
| } | |||||
| function onSampleEditTypesChange() { | |||||
| if (!sampleEditShowWaxReturn.value) { | |||||
| sampleEditForm.wax_return = 0; | |||||
| sampleEditForm.return_name = ''; | |||||
| sampleEditForm.return_phone = ''; | |||||
| sampleEditForm.returnRegionCodes = []; | |||||
| sampleEditForm.return_address = ''; | |||||
| } | |||||
| if (!sampleEditForm.sample_types || !sampleEditForm.sample_types.length) { | |||||
| sampleEditForm.report_email = ''; | |||||
| sampleEditForm.sample_tracking_no = ''; | |||||
| sampleEditForm.sample_photos = []; | |||||
| } | |||||
| } | |||||
| function showSampleEditDialog() { | |||||
| Object.assign(sampleEditForm, { | |||||
| sample_types: (patient.sample_types || []).slice(), | |||||
| wax_return: patient.wax_return ? 1 : 0, | |||||
| return_name: patient.return_name || '', | |||||
| return_phone: patient.return_phone || '', | |||||
| returnRegionCodes: [patient.return_province_code, patient.return_city_code, patient.return_district_code].filter(Boolean), | |||||
| return_address: patient.return_address || '', | |||||
| report_email: patient.report_email || '', | |||||
| sample_tracking_no: patient.sample_tracking_no || '', | |||||
| sample_photos: (patient.sample_photos || []).slice() | |||||
| }); | |||||
| sampleEditVisible.value = true; | |||||
| } | |||||
| function onSampleEditPhotoUpload(res) { | |||||
| if (res.code === 0 && res.data && res.data.url) { | |||||
| sampleEditForm.sample_photos.push(res.data.url); | |||||
| } else { | |||||
| ElementPlus.ElMessage.error(res.msg || '上传失败'); | |||||
| } | |||||
| } | |||||
| async function saveSampleEdit() { | |||||
| var regionCodes = sampleEditForm.returnRegionCodes || []; | |||||
| if (sampleEditShowWaxReturn.value && sampleEditForm.wax_return === 1) { | |||||
| if (!sampleEditForm.return_name.trim()) return ElementPlus.ElMessage.warning('请填写回寄收件人'); | |||||
| if (!sampleEditForm.return_phone.trim()) return ElementPlus.ElMessage.warning('请填写回寄电话'); | |||||
| if (regionCodes.length !== 3) return ElementPlus.ElMessage.warning('请选择回寄地区'); | |||||
| if (!sampleEditForm.return_address.trim()) return ElementPlus.ElMessage.warning('请填写回寄详细地址'); | |||||
| } | |||||
| if (sampleEditForm.report_email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(sampleEditForm.report_email)) { | |||||
| return ElementPlus.ElMessage.warning('邮箱格式不正确'); | |||||
| } | |||||
| sampleEditSaving.value = true; | |||||
| try { | |||||
| var res = await fetch('/admin/patient/editSampleInfo', { | |||||
| method: 'POST', | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify({ | |||||
| id: patient.id, | |||||
| sample_types: sampleEditForm.sample_types || [], | |||||
| wax_return: sampleEditShowWaxReturn.value ? sampleEditForm.wax_return : 0, | |||||
| return_name: sampleEditForm.return_name.trim(), | |||||
| return_phone: sampleEditForm.return_phone.trim(), | |||||
| return_province_code: regionCodes[0] || '', | |||||
| return_city_code: regionCodes[1] || '', | |||||
| return_district_code: regionCodes[2] || '', | |||||
| return_address: sampleEditForm.return_address.trim(), | |||||
| report_email: sampleEditForm.report_email.trim(), | |||||
| sample_tracking_no: sampleEditForm.sample_tracking_no.trim(), | |||||
| sample_photos: sampleEditForm.sample_photos || [] | |||||
| }) | |||||
| }).then(function(r) { return r.json(); }); | |||||
| if (res.code === 0) { | |||||
| ElementPlus.ElMessage.success('送检信息已保存并生效'); | |||||
| sampleEditVisible.value = false; | |||||
| loadDetail(); | |||||
| } else { | |||||
| ElementPlus.ElMessage.error(res.msg || '保存失败'); | |||||
| } | |||||
| } finally { | |||||
| sampleEditSaving.value = false; | |||||
| } | |||||
| } | |||||
| onMounted(function() { | |||||
| loadDetail(); | |||||
| loadSampleTypes(); | |||||
| loadRegions(); | |||||
| }); | |||||
| return { | return { | ||||
| loading, patient, audits, canAudit, canEdit, | loading, patient, audits, canAudit, canEdit, | ||||
| returnTrackingVisible, returnTrackingSaving, returnTrackingNo, | returnTrackingVisible, returnTrackingSaving, returnTrackingNo, | ||||
| sampleRejectVisible, sampleRejectSaving, sampleRejectReason, | |||||
| sampleEditVisible, sampleEditSaving, sampleEditForm, sampleTypeList, regionTree, uploadHeaders, | |||||
| rejectVisible, rejectSaving, rejectReason, selectedReasons, commonReasons, | rejectVisible, rejectSaving, rejectReason, selectedReasons, commonReasons, | ||||
| hospitalRegionText, goBack, downloadSign, isImageUrl, signDocs, signImageList, | hospitalRegionText, goBack, downloadSign, isImageUrl, signDocs, signImageList, | ||||
| authImageList, sampleInfoStatusText, sampleInfoStatusType, | |||||
| authImageList, sampleInfoStatusText, sampleInfoStatusType, sampleEditShowWaxReturn, sampleEditNeedReturnNames, | |||||
| handleApprove, showRejectDialog, toggleReason, doReject, resetSampleInfo, | handleApprove, showRejectDialog, toggleReason, doReject, resetSampleInfo, | ||||
| showReturnTrackingDialog, saveReturnTrackingNo | |||||
| showReturnTrackingDialog, saveReturnTrackingNo, | |||||
| approveSampleEdit, showRejectSampleEditDialog, rejectSampleEdit, | |||||
| showSampleEditDialog, onSampleEditTypesChange, onSampleEditPhotoUpload, saveSampleEdit | |||||
| }; | }; | ||||
| } | } | ||||
| }); | }); | ||||
| @@ -90,11 +90,16 @@ | |||||
| <el-tag v-else-if="row.status === 2" type="danger" size="small">已驳回</el-tag> | <el-tag v-else-if="row.status === 2" type="danger" size="small">已驳回</el-tag> | ||||
| </template> | </template> | ||||
| </el-table-column> | </el-table-column> | ||||
| <el-table-column label="送检状态" min-width="130" align="center"> | |||||
| <template #default="{ row }"> | |||||
| <el-tag :type="row.sample_info_status_type || 'info'" size="small">${ row.sample_info_status_text || '未提交' }</el-tag> | |||||
| </template> | |||||
| </el-table-column> | |||||
| <el-table-column label="操作" width="240" align="center" fixed="right"> | <el-table-column label="操作" width="240" align="center" fixed="right"> | ||||
| <template #default="{ row }"> | <template #default="{ row }"> | ||||
| <el-button type="primary" link @click="viewDetail(row)">查看详情</el-button> | <el-button type="primary" link @click="viewDetail(row)">查看详情</el-button> | ||||
| <el-button v-if="perms.canEdit" type="info" link @click="showEditDialog(row)">编辑</el-button> | <el-button v-if="perms.canEdit" type="info" link @click="showEditDialog(row)">编辑</el-button> | ||||
| <el-button v-if="row.status === 0 && perms.canAudit" type="warning" link @click="viewDetail(row)">去审核</el-button> | |||||
| <el-button v-if="(row.status === 0 || row.sample_info_status == 3) && perms.canAudit" type="warning" link @click="viewDetail(row)">去审核</el-button> | |||||
| <el-button v-if="perms.canDelete" type="danger" link @click="handleDelete(row)">删除</el-button> | <el-button v-if="perms.canDelete" type="danger" link @click="handleDelete(row)">删除</el-button> | ||||
| </template> | </template> | ||||
| </el-table-column> | </el-table-column> | ||||
| @@ -348,7 +353,7 @@ const app = createApp({ | |||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const tableData = ref([]); | const tableData = ref([]); | ||||
| const pagination = reactive({ page: 1, pageSize: 10, total: 0 }); | const pagination = reactive({ page: 1, pageSize: 10, total: 0 }); | ||||
| const counts = reactive({ all: 0, pending: 0, approved: 0, rejected: 0 }); | |||||
| const counts = reactive({ all: 0, draft: 0, pending: 0, approved: 0, rejected: 0 }); | |||||
| const uploadHeaders = {}; | const uploadHeaders = {}; | ||||
| // 新增弹窗 | // 新增弹窗 | ||||