| @@ -2,7 +2,7 @@ | |||
| CREATE TABLE `message` ( | |||
| `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, | |||
| `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 '标题', | |||
| `content` TEXT 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`; | |||
| -- 更新 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/resetSampleInfo', 'admin/patient/resetSampleInfo', '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'], | |||
| // 公共接口 | |||
| ['/common/regions', 'common/regions'], | |||
| @@ -83,6 +86,7 @@ module.exports = [ | |||
| ['/api/mp/subscribeConfig', 'mp/subscribeConfig'], | |||
| ['/api/mp/sampleInfo', 'mp/sampleInfo'], | |||
| ['/api/mp/saveSampleInfo', 'mp/saveSampleInfo', 'post'], | |||
| ['/api/mp/applySampleInfoEdit', 'mp/applySampleInfoEdit', 'post'], | |||
| ['/api/mp/regenerateSign', 'mp/regenerateSign'], | |||
| ['/api/mp/regenerateSignByUrl', 'mp/regenerateSignByUrl', 'post'], | |||
| @@ -1,13 +1,119 @@ | |||
| const Base = require('../base.js'); | |||
| const dayjs = require('dayjs'); | |||
| const APP_REMARK = 'pap_mini_cytx'; | |||
| const AUDIT_APPROVED_CONTENT = '您提交的肠愈同行患者关爱项目资料已审核通过,请于7天内在小程序-个人中心-送检信息中提交相关信息。'; | |||
| const SAMPLE_EDIT_AUDIT_TEMPLATE_ID = 'OcMLIGS8pMnpGzZmPkdMELDgFefqOeQ-1jsit8Ldw2w'; | |||
| module.exports = class extends Base { | |||
| _canEditPatient() { | |||
| 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() { | |||
| this.assign('currentPage', 'patient'); | |||
| @@ -60,6 +166,8 @@ module.exports = class extends Base { | |||
| const cName = regionMap[item.city_code] || ''; | |||
| const dName = regionMap[item.district_code] || ''; | |||
| 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 }); | |||
| @@ -125,13 +233,21 @@ module.exports = class extends Base { | |||
| 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') | |||
| .where({ patient_id: id }) | |||
| .order('id DESC') | |||
| .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() { | |||
| @@ -372,9 +488,14 @@ module.exports = class extends Base { | |||
| 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_reset_edit', '重置送检信息编辑', '后台重置送检信息为可修改'); | |||
| await this.log('edit', '患者管理', `重置送检信息编辑权限(ID:${id})`); | |||
| return this.success(); | |||
| } | |||
| @@ -409,10 +530,119 @@ module.exports = class extends Base { | |||
| is_read: 0 | |||
| }); | |||
| await this._addSampleLog(id, 'sample_return', Number(patient.sample_info_status) === 2 ? '修改回寄物流单号' : '填写回寄物流单号', `回寄物流单号:${trackingNo}`); | |||
| await this.log('edit', '患者管理', `填写回寄物流单号(ID:${id}):${trackingNo}`); | |||
| 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() { | |||
| const data = this.post(); | |||
| @@ -617,7 +847,6 @@ module.exports = class extends Base { | |||
| // @private 发送审核结果订阅消息 | |||
| async _sendAuditSubscribeMessage(patientId, patientName, result) { | |||
| try { | |||
| const APP_REMARK = 'pap_mini_cytx'; | |||
| // 查找该患者关联的微信用户 | |||
| const wechatUser = await this.model('wechat_user') | |||
| .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 发送审核结果短信通知 | |||
| async _sendAuditSms(phone, type, reason) { | |||
| if (!phone) return; | |||
| @@ -7,8 +7,29 @@ const cosConfig = require('../config/cos.js'); | |||
| const APP_REMARK = 'pap_mini_cytx'; | |||
| const AUDIT_APPROVED_CONTENT = '您提交的肠愈同行患者关爱项目资料已审核通过,请于7天内在小程序-个人中心-送检信息中提交相关信息。'; | |||
| const SAMPLE_EDIT_AUDIT_TEMPLATE_ID = 'OcMLIGS8pMnpGzZmPkdMELDgFefqOeQ-1jsit8Ldw2w'; | |||
| 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 | |||
| async loginAction() { | |||
| const code = this.post('code'); | |||
| @@ -672,10 +693,11 @@ module.exports = class extends Base { | |||
| try { | |||
| const wechatService = this.service('wechat'); | |||
| 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 }); | |||
| } catch (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, | |||
| return_tracking_no: patient.return_tracking_no || '', | |||
| 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 | |||
| }}); | |||
| } catch (error) { | |||
| @@ -768,16 +794,30 @@ module.exports = class extends Base { | |||
| } | |||
| if (sampleInfoStatus === 1) { | |||
| const contactPhone = sampleReceiverInfo.contact_phone || ''; | |||
| const msg = '送检信息已生效,如需修改请点击页面的【申请修改送检信息】按钮申请,通过后可重新提交送检信息。' + | |||
| (contactPhone ? `详情咨询${contactPhone}。` : ''); | |||
| return this.json({ | |||
| code: 1010, | |||
| 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 hadSampleInfo = this._hasSampleInfo(patient); | |||
| const now = think.datetime(new Date()); | |||
| // 邮箱格式与重复校验 | |||
| if (hasSample && report_email) { | |||
| @@ -802,7 +842,22 @@ module.exports = class extends Base { | |||
| report_email: hasSample ? (report_email || '') : '', | |||
| sample_tracking_no: hasSample ? (sample_tracking_no || '') : '', | |||
| 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: '提交成功' }); | |||
| @@ -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 | |||
| @@ -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 .time { font-size: 12px; color: #909399; margin-bottom: 4px; } | |||
| .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; } | |||
| .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> | |||
| {% 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_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_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 v-if="patient.sample_photos && patient.sample_photos.length" style="margin-top:16px;"> | |||
| <div style="font-size:13px;color:#909399;margin-bottom:8px;">送检单照片</div> | |||
| @@ -184,19 +192,18 @@ | |||
| </div> | |||
| </div> | |||
| <!-- 审核记录 --> | |||
| <!-- 流转日志 --> | |||
| <div class="detail-panel"> | |||
| <h3>审核记录</h3> | |||
| <h3>流转日志</h3> | |||
| <ul class="timeline-list" v-if="audits.length"> | |||
| <li v-for="(item, idx) in audits" :key="idx"> | |||
| <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> | |||
| </ul> | |||
| <el-empty v-else description="暂无审核记录" :image-size="60" /> | |||
| <el-empty v-else description="暂无流转日志" :image-size="60" /> | |||
| </div> | |||
| </div> | |||
| @@ -205,6 +212,9 @@ | |||
| <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 === 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.wax_return && (patient.sample_info_status == 1 || patient.sample_info_status == 2) && canEdit" type="primary" @click="showReturnTrackingDialog"> | |||
| ${ patient.sample_info_status == 2 ? '修改回寄物流单号' : '填写回寄物流单号' } | |||
| @@ -239,6 +249,75 @@ | |||
| </div> | |||
| </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 %} | |||
| {% block js %} | |||
| @@ -269,6 +348,8 @@ var app = createApp({ | |||
| return_address: '', | |||
| report_email: '', sample_tracking_no: '', sample_photos: [], | |||
| 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: '', | |||
| emergency_contact: '', emergency_phone: '', | |||
| status: -1, create_time: '' | |||
| @@ -280,7 +361,27 @@ var app = createApp({ | |||
| var returnTrackingVisible = ref(false); | |||
| var returnTrackingSaving = ref(false); | |||
| 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() { | |||
| 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); | |||
| }); | |||
| 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) { | |||
| if (Number(status) === 3) return '修改申请待审核'; | |||
| if (Number(status) === 2) return '已寄回'; | |||
| if (Number(status) === 1) return '已生效'; | |||
| return '可修改'; | |||
| } | |||
| function sampleInfoStatusType(status) { | |||
| if (Number(status) === 3) return 'warning'; | |||
| if (Number(status) === 2) return 'primary'; | |||
| if (Number(status) === 1) return 'success'; | |||
| 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 { | |||
| loading, patient, audits, canAudit, canEdit, | |||
| returnTrackingVisible, returnTrackingSaving, returnTrackingNo, | |||
| sampleRejectVisible, sampleRejectSaving, sampleRejectReason, | |||
| sampleEditVisible, sampleEditSaving, sampleEditForm, sampleTypeList, regionTree, uploadHeaders, | |||
| rejectVisible, rejectSaving, rejectReason, selectedReasons, commonReasons, | |||
| hospitalRegionText, goBack, downloadSign, isImageUrl, signDocs, signImageList, | |||
| authImageList, sampleInfoStatusText, sampleInfoStatusType, | |||
| authImageList, sampleInfoStatusText, sampleInfoStatusType, sampleEditShowWaxReturn, sampleEditNeedReturnNames, | |||
| 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> | |||
| </template> | |||
| </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"> | |||
| <template #default="{ row }"> | |||
| <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="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> | |||
| </template> | |||
| </el-table-column> | |||
| @@ -348,7 +353,7 @@ const app = createApp({ | |||
| const loading = ref(false); | |||
| const tableData = ref([]); | |||
| 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 = {}; | |||
| // 新增弹窗 | |||