| @@ -0,0 +1,37 @@ | |||||
| -- 患者表新增字段 | |||||
| ALTER TABLE `patient` | |||||
| ADD COLUMN `hospital` varchar(100) DEFAULT '' COMMENT '医院名称' AFTER `address`, | |||||
| ADD COLUMN `sample_types` varchar(255) DEFAULT '' COMMENT '送检样本类型(JSON数组)', | |||||
| ADD COLUMN `wax_return` tinyint(1) DEFAULT 0 COMMENT '是否需寄回: 0否 1是', | |||||
| ADD COLUMN `return_name` varchar(50) DEFAULT '' COMMENT '寄回收件人姓名', | |||||
| ADD COLUMN `return_phone` varchar(20) DEFAULT '' COMMENT '寄回收件人电话', | |||||
| ADD COLUMN `return_province_code` varchar(10) DEFAULT '' COMMENT '寄回省份编码', | |||||
| ADD COLUMN `return_city_code` varchar(10) DEFAULT '' COMMENT '寄回城市编码', | |||||
| ADD COLUMN `return_district_code` varchar(10) DEFAULT '' COMMENT '寄回区县编码', | |||||
| ADD COLUMN `return_address` varchar(255) DEFAULT '' COMMENT '寄回详细地址', | |||||
| ADD COLUMN `report_email` varchar(100) DEFAULT '' COMMENT '报告接收邮箱', | |||||
| ADD COLUMN `sample_tracking_no` varchar(100) DEFAULT '' COMMENT '送检样本物流单号', | |||||
| ADD COLUMN `sample_photos` json DEFAULT NULL COMMENT '送检单照片(JSON数组)'; | |||||
| -- 送检样本类型配置表 | |||||
| CREATE TABLE `sample_type` ( | |||||
| `id` int(11) NOT NULL AUTO_INCREMENT, | |||||
| `name` varchar(50) NOT NULL COMMENT '样本类型名称', | |||||
| `need_return` tinyint(1) DEFAULT 0 COMMENT '是否可选需寄回: 0否 1是', | |||||
| `sort` int(11) DEFAULT 0 COMMENT '排序(越大越前)', | |||||
| `status` tinyint(1) DEFAULT 1 COMMENT '状态: 1启用 0禁用', | |||||
| `is_deleted` tinyint(1) DEFAULT 0 COMMENT '软删除', | |||||
| `create_time` datetime DEFAULT CURRENT_TIMESTAMP, | |||||
| `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |||||
| PRIMARY KEY (`id`) | |||||
| ) COMMENT='送检样本类型配置'; | |||||
| INSERT INTO `sample_type` (`name`, `need_return`, `sort`, `status`) VALUES | |||||
| ('蜡块', 1, 4, 1), | |||||
| ('白片', 0, 3, 1), | |||||
| ('血液', 0, 2, 1), | |||||
| ('新鲜组织', 0, 1, 1); | |||||
| -- sys_config 新增送检样本是否必选配置 | |||||
| INSERT INTO `sys_config` (`config_key`, `config_value`, `remark`) VALUES | |||||
| ('sample_required', '0', '送检样本是否必选: 0否 1是'); | |||||
| @@ -55,6 +55,7 @@ module.exports = [ | |||||
| ['/common/regions', 'common/regions'], | ['/common/regions', 'common/regions'], | ||||
| ['/common/ocr/idcard', 'common/ocrIdcard', 'post'], | ['/common/ocr/idcard', 'common/ocrIdcard', 'post'], | ||||
| ['/common/ocr/hmt', 'common/ocrHmt', 'post'], | ['/common/ocr/hmt', 'common/ocrHmt', 'post'], | ||||
| ['/common/sampleTypes', 'common/sampleTypes'], | |||||
| ['/api/content', 'common/content'], | ['/api/content', 'common/content'], | ||||
| // 小程序接口 | // 小程序接口 | ||||
| @@ -85,6 +86,15 @@ module.exports = [ | |||||
| ['/admin/tag/delete', 'admin/tag/delete', 'post'], | ['/admin/tag/delete', 'admin/tag/delete', 'post'], | ||||
| ['/admin/tag/sort', 'admin/tag/sort', 'post'], | ['/admin/tag/sort', 'admin/tag/sort', 'post'], | ||||
| // 送检样本管理 | |||||
| ['/admin/sample_type', 'admin/sample_type/index'], | |||||
| ['/admin/sample_type/list', 'admin/sample_type/list'], | |||||
| ['/admin/sample_type/add', 'admin/sample_type/add', 'post'], | |||||
| ['/admin/sample_type/edit', 'admin/sample_type/edit', 'post'], | |||||
| ['/admin/sample_type/delete', 'admin/sample_type/delete', 'post'], | |||||
| ['/admin/sample_type/sort', 'admin/sample_type/sort', 'post'], | |||||
| ['/admin/sample_type/setRequired', 'admin/sample_type/setRequired', 'post'], | |||||
| // 内容管理 | // 内容管理 | ||||
| ['/admin/content', 'admin/content/index'], | ['/admin/content', 'admin/content/index'], | ||||
| ['/admin/content/list', 'admin/content/list'], | ['/admin/content/list', 'admin/content/list'], | ||||
| @@ -91,25 +91,27 @@ module.exports = class extends Base { | |||||
| } | } | ||||
| // 解析 JSON 字段 | // 解析 JSON 字段 | ||||
| try { | |||||
| patient.documents = JSON.parse(patient.documents || '[]'); | |||||
| } catch (e) { | |||||
| patient.documents = []; | |||||
| } | |||||
| // 查询省市区名称 | |||||
| if (patient.province_code || patient.city_code || patient.district_code) { | |||||
| const codes = [patient.province_code, patient.city_code, patient.district_code].filter(Boolean); | |||||
| if (codes.length) { | |||||
| const regions = await this.model('sys_region') | |||||
| .where({ code: ['in', codes] }) | |||||
| .select(); | |||||
| const regionMap = {}; | |||||
| regions.forEach(r => { regionMap[r.code] = r.name; }); | |||||
| patient.province_name = regionMap[patient.province_code] || ''; | |||||
| patient.city_name = regionMap[patient.city_code] || ''; | |||||
| patient.district_name = regionMap[patient.district_code] || ''; | |||||
| } | |||||
| try { patient.documents = JSON.parse(patient.documents || '[]'); } catch (e) { patient.documents = []; } | |||||
| try { patient.sample_types = JSON.parse(patient.sample_types || '[]'); } catch (e) { patient.sample_types = []; } | |||||
| try { patient.sample_photos = JSON.parse(patient.sample_photos || '[]'); } catch (e) { patient.sample_photos = []; } | |||||
| // 查询省市区名称(包含寄回地址) | |||||
| const allCodes = [ | |||||
| patient.province_code, patient.city_code, patient.district_code, | |||||
| patient.return_province_code, patient.return_city_code, patient.return_district_code | |||||
| ].filter(Boolean); | |||||
| if (allCodes.length) { | |||||
| const regions = await this.model('sys_region') | |||||
| .where({ code: ['in', allCodes] }) | |||||
| .select(); | |||||
| const regionMap = {}; | |||||
| regions.forEach(r => { regionMap[r.code] = r.name; }); | |||||
| patient.province_name = regionMap[patient.province_code] || ''; | |||||
| patient.city_name = regionMap[patient.city_code] || ''; | |||||
| patient.district_name = regionMap[patient.district_code] || ''; | |||||
| patient.return_province_name = regionMap[patient.return_province_code] || ''; | |||||
| patient.return_city_name = regionMap[patient.return_city_code] || ''; | |||||
| patient.return_district_name = regionMap[patient.return_district_code] || ''; | |||||
| } | } | ||||
| // 获取审核记录 | // 获取审核记录 | ||||
| @@ -124,7 +126,7 @@ module.exports = class extends Base { | |||||
| // 新增患者 | // 新增患者 | ||||
| async addAction() { | async addAction() { | ||||
| const data = this.post(); | const data = this.post(); | ||||
| const { name, phone, id_card, gender, birth_date, province_code, city_code, district_code, address, emergency_contact, emergency_phone, tag, documents, sign_income, sign_privacy, sign_promise, sign_privacy_jhr, income_amount, guardian_name, guardian_id_card, guardian_relation } = data; | |||||
| const { name, phone, id_card, gender, birth_date, province_code, city_code, district_code, address, hospital, emergency_contact, emergency_phone, tag, documents, 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, sign_income, sign_privacy, sign_promise, sign_privacy_jhr, income_amount, guardian_name, guardian_id_card, guardian_relation } = data; | |||||
| if (!name || !phone || !id_card || !gender || !birth_date) { | if (!name || !phone || !id_card || !gender || !birth_date) { | ||||
| return this.fail('请填写完整信息'); | return this.fail('请填写完整信息'); | ||||
| @@ -175,10 +177,22 @@ module.exports = class extends Base { | |||||
| city_code: city_code || '', | city_code: city_code || '', | ||||
| district_code: district_code || '', | district_code: district_code || '', | ||||
| address: address || '', | address: address || '', | ||||
| hospital: hospital || '', | |||||
| emergency_contact: emergency_contact || '', | emergency_contact: emergency_contact || '', | ||||
| emergency_phone: emergency_phone || '', | emergency_phone: emergency_phone || '', | ||||
| tag: tag || '', | tag: tag || '', | ||||
| documents: JSON.stringify(documents || []), | documents: JSON.stringify(documents || []), | ||||
| sample_types: JSON.stringify(sample_types || []), | |||||
| wax_return: wax_return ? 1 : 0, | |||||
| return_name: return_name || '', | |||||
| return_phone: return_phone || '', | |||||
| return_province_code: return_province_code || '', | |||||
| return_city_code: return_city_code || '', | |||||
| return_district_code: return_district_code || '', | |||||
| return_address: return_address || '', | |||||
| report_email: report_email || '', | |||||
| sample_tracking_no: sample_tracking_no || '', | |||||
| sample_photos: JSON.stringify(sample_photos || []), | |||||
| sign_income: sign_income || '', | sign_income: sign_income || '', | ||||
| sign_privacy: sign_privacy || '', | sign_privacy: sign_privacy || '', | ||||
| sign_promise: sign_promise || '', | sign_promise: sign_promise || '', | ||||
| @@ -318,7 +332,7 @@ module.exports = class extends Base { | |||||
| // 编辑患者 | // 编辑患者 | ||||
| async editAction() { | async editAction() { | ||||
| const data = this.post(); | const data = this.post(); | ||||
| const { id, name, phone, id_card, gender, birth_date, province_code, city_code, district_code, address, emergency_contact, emergency_phone, tag, documents, sign_income, sign_privacy, sign_promise, sign_privacy_jhr, income_amount, guardian_name, guardian_id_card, guardian_relation } = data; | |||||
| const { id, name, phone, id_card, gender, birth_date, province_code, city_code, district_code, address, hospital, emergency_contact, emergency_phone, tag, documents, 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, sign_income, sign_privacy, sign_promise, sign_privacy_jhr, income_amount, guardian_name, guardian_id_card, guardian_relation } = data; | |||||
| if (!id) return this.fail('参数错误'); | if (!id) return this.fail('参数错误'); | ||||
| if (!name || !phone || !id_card || !gender || !birth_date) return this.fail('请填写完整信息'); | if (!name || !phone || !id_card || !gender || !birth_date) return this.fail('请填写完整信息'); | ||||
| @@ -340,10 +354,22 @@ module.exports = class extends Base { | |||||
| name, phone, id_card, gender, birth_date, | name, phone, id_card, gender, birth_date, | ||||
| province_code, city_code, district_code, | province_code, city_code, district_code, | ||||
| address: address || '', | address: address || '', | ||||
| hospital: hospital || '', | |||||
| emergency_contact: emergency_contact || '', | emergency_contact: emergency_contact || '', | ||||
| emergency_phone: emergency_phone || '', | emergency_phone: emergency_phone || '', | ||||
| tag: tag || '', | tag: tag || '', | ||||
| documents: JSON.stringify(documents || []), | documents: JSON.stringify(documents || []), | ||||
| sample_types: JSON.stringify(sample_types || []), | |||||
| wax_return: wax_return ? 1 : 0, | |||||
| return_name: return_name || '', | |||||
| return_phone: return_phone || '', | |||||
| return_province_code: return_province_code || '', | |||||
| return_city_code: return_city_code || '', | |||||
| return_district_code: return_district_code || '', | |||||
| return_address: return_address || '', | |||||
| report_email: report_email || '', | |||||
| sample_tracking_no: sample_tracking_no || '', | |||||
| sample_photos: JSON.stringify(sample_photos || []), | |||||
| sign_income: sign_income || '', | sign_income: sign_income || '', | ||||
| sign_privacy: sign_privacy || '', | sign_privacy: sign_privacy || '', | ||||
| sign_promise: sign_promise || '', | sign_promise: sign_promise || '', | ||||
| @@ -365,12 +391,15 @@ module.exports = class extends Base { | |||||
| const model = this.model('patient'); | const model = this.model('patient'); | ||||
| const list = await model.getAll({ keyword, tag, status, startDate, endDate, province_code, city_code, district_code }); | const list = await model.getAll({ keyword, tag, status, startDate, endDate, province_code, city_code, district_code }); | ||||
| // 批量查省市区名称 | |||||
| // 批量查省市区名称(包含寄回地址) | |||||
| const allCodes = new Set(); | const allCodes = new Set(); | ||||
| list.forEach(item => { | list.forEach(item => { | ||||
| if (item.province_code) allCodes.add(item.province_code); | if (item.province_code) allCodes.add(item.province_code); | ||||
| if (item.city_code) allCodes.add(item.city_code); | if (item.city_code) allCodes.add(item.city_code); | ||||
| if (item.district_code) allCodes.add(item.district_code); | if (item.district_code) allCodes.add(item.district_code); | ||||
| if (item.return_province_code) allCodes.add(item.return_province_code); | |||||
| if (item.return_city_code) allCodes.add(item.return_city_code); | |||||
| if (item.return_district_code) allCodes.add(item.return_district_code); | |||||
| }); | }); | ||||
| const regionMap = {}; | const regionMap = {}; | ||||
| if (allCodes.size) { | if (allCodes.size) { | ||||
| @@ -394,7 +423,13 @@ module.exports = class extends Base { | |||||
| } | } | ||||
| const statusMap = { '-1': '待提交', 0: '待审核', 1: '审核通过', 2: '已驳回' }; | const statusMap = { '-1': '待提交', 0: '待审核', 1: '审核通过', 2: '已驳回' }; | ||||
| const header = ['ID', '姓名', '性别', '身份证', '手机号', '省份', '城市', '提交时间', '审核状态', '审核日期', '审核驳回原因', '瘤种']; | |||||
| const header = [ | |||||
| 'ID', '姓名', '性别', '身份证', '手机号', '省份', '城市', '详细地址', | |||||
| '医院名称', '紧急联系人', '紧急联系电话', '瘤种', | |||||
| '送检样本类型', '是否需寄回', '收件人', '收件电话', '收件地址', | |||||
| '报告接收邮箱', '送检物流单号', | |||||
| '提交时间', '审核状态', '审核日期', '审核驳回原因' | |||||
| ]; | |||||
| const ExcelJS = require('exceljs'); | const ExcelJS = require('exceljs'); | ||||
| const workbook = new ExcelJS.Workbook(); | const workbook = new ExcelJS.Workbook(); | ||||
| @@ -402,9 +437,9 @@ module.exports = class extends Base { | |||||
| // 表头 | // 表头 | ||||
| sheet.addRow(header); | sheet.addRow(header); | ||||
| // 表头样式:绿色背景、白色加粗字体(仅到瘤种列,即K列=11列)、冻结首行 | |||||
| // 表头样式:绿色背景、白色加粗字体、冻结首行 | |||||
| const headerRow = sheet.getRow(1); | const headerRow = sheet.getRow(1); | ||||
| const colCount = header.length; // 11列 | |||||
| const colCount = header.length; | |||||
| for (let i = 1; i <= colCount; i++) { | for (let i = 1; i <= colCount; i++) { | ||||
| const cell = headerRow.getCell(i); | const cell = headerRow.getCell(i); | ||||
| cell.font = { bold: true, color: { argb: 'FFFFFFFF' } }; | cell.font = { bold: true, color: { argb: 'FFFFFFFF' } }; | ||||
| @@ -428,6 +463,12 @@ module.exports = class extends Base { | |||||
| // 数据行 | // 数据行 | ||||
| list.forEach(item => { | list.forEach(item => { | ||||
| const audit = auditMap[item.id]; | const audit = auditMap[item.id]; | ||||
| let sampleTypes = ''; | |||||
| try { sampleTypes = JSON.parse(item.sample_types || '[]').join('、'); } catch (e) { sampleTypes = ''; } | |||||
| const returnAddr = item.return_address | |||||
| ? [regionMap[item.return_province_code] || '', regionMap[item.return_city_code] || '', regionMap[item.return_district_code] || '', item.return_address].filter(Boolean).join('') | |||||
| : ''; | |||||
| const row = sheet.addRow([ | const row = sheet.addRow([ | ||||
| item.patient_no, | item.patient_no, | ||||
| item.name, | item.name, | ||||
| @@ -436,11 +477,22 @@ module.exports = class extends Base { | |||||
| item.phone, | item.phone, | ||||
| regionMap[item.province_code] || '', | regionMap[item.province_code] || '', | ||||
| regionMap[item.city_code] || '', | regionMap[item.city_code] || '', | ||||
| item.address || '', | |||||
| item.hospital || '', | |||||
| item.emergency_contact || '', | |||||
| item.emergency_phone || '', | |||||
| item.tag || '', | |||||
| sampleTypes, | |||||
| item.wax_return ? '是' : '否', | |||||
| item.return_name || '', | |||||
| item.return_phone || '', | |||||
| returnAddr, | |||||
| item.report_email || '', | |||||
| item.sample_tracking_no || '', | |||||
| item.create_time || '', | item.create_time || '', | ||||
| statusMap[item.status] || '', | statusMap[item.status] || '', | ||||
| audit ? (audit.create_time || '') : '', | audit ? (audit.create_time || '') : '', | ||||
| (audit && audit.action === 'reject') ? (audit.reason || '') : '', | |||||
| item.tag || '' | |||||
| (audit && audit.action === 'reject') ? (audit.reason || '') : '' | |||||
| ]); | ]); | ||||
| // 数据行加边框 | // 数据行加边框 | ||||
| for (let i = 1; i <= colCount; i++) { | for (let i = 1; i <= colCount; i++) { | ||||
| @@ -0,0 +1,75 @@ | |||||
| const Base = require('../base'); | |||||
| module.exports = class extends Base { | |||||
| async indexAction() { | |||||
| this.assign('currentPage', 'sample-type'); | |||||
| this.assign('pageTitle', '送检样本管理'); | |||||
| this.assign('breadcrumb', [ | |||||
| { name: '系统管理', url: '/admin/system/user.html' }, | |||||
| { name: '送检样本管理' } | |||||
| ]); | |||||
| // 获取当前必选配置 | |||||
| const required = await this.model('sys_config').getByKey('sample_required'); | |||||
| this.assign('sampleRequired', required === '1' || required === 1 ? '1' : '0'); | |||||
| return this.display(); | |||||
| } | |||||
| async listAction() { | |||||
| const list = await this.model('sample_type') | |||||
| .where({ is_deleted: 0 }) | |||||
| .order('sort DESC, id ASC') | |||||
| .select(); | |||||
| return this.json({ code: 0, data: list }); | |||||
| } | |||||
| async addAction() { | |||||
| const { name, need_return, sort } = this.post(); | |||||
| if (!name) return this.fail('请输入样本类型名称'); | |||||
| await this.model('sample_type').add({ | |||||
| name, | |||||
| need_return: need_return ? 1 : 0, | |||||
| sort: sort || 0, | |||||
| status: 1 | |||||
| }); | |||||
| await this.log('add', '送检样本管理', `新增样本类型: ${name}`); | |||||
| return this.success(); | |||||
| } | |||||
| async editAction() { | |||||
| const { id, name, need_return, sort, status } = this.post(); | |||||
| if (!id) return this.fail('参数错误'); | |||||
| if (!name) return this.fail('请输入样本类型名称'); | |||||
| await this.model('sample_type').where({ id }).update({ | |||||
| name, | |||||
| need_return: need_return ? 1 : 0, | |||||
| sort: sort || 0, | |||||
| status: status !== undefined ? parseInt(status) : 1 | |||||
| }); | |||||
| await this.log('edit', '送检样本管理', `编辑样本类型(ID:${id}): ${name}`); | |||||
| return this.success(); | |||||
| } | |||||
| async deleteAction() { | |||||
| const { id } = this.post(); | |||||
| if (!id) return this.fail('参数错误'); | |||||
| await this.model('sample_type').where({ id }).update({ is_deleted: 1 }); | |||||
| await this.log('edit', '送检样本管理', `删除样本类型(ID:${id})`); | |||||
| return this.success(); | |||||
| } | |||||
| async sortAction() { | |||||
| const { ids } = this.post(); | |||||
| if (!ids || !ids.length) return this.fail('参数错误'); | |||||
| for (let i = 0; i < ids.length; i++) { | |||||
| await this.model('sample_type').where({ id: ids[i] }).update({ sort: ids.length - i }); | |||||
| } | |||||
| return this.success(); | |||||
| } | |||||
| async setRequiredAction() { | |||||
| const { value } = this.post(); | |||||
| await this.model('sys_config').setByKey('sample_required', value ? '1' : '0', '送检样本是否必选'); | |||||
| await this.log('edit', '送检样本管理', `设置送检样本必选: ${value ? '是' : '否'}`); | |||||
| return this.success(); | |||||
| } | |||||
| }; | |||||
| @@ -149,6 +149,21 @@ module.exports = class extends think.Controller { | |||||
| } | } | ||||
| } | } | ||||
| /** | |||||
| * 获取送检样本类型列表及配置 | |||||
| * GET /common/sampleTypes | |||||
| */ | |||||
| async sampleTypesAction() { | |||||
| try { | |||||
| const list = await this.model('sample_type').getEnabledList(); | |||||
| const required = await this.model('sys_config').getByKey('sample_required'); | |||||
| return this.json({ code: 0, data: { list, required: required === '1' || required === 1 } }); | |||||
| } catch (error) { | |||||
| think.logger.error('获取送检样本类型失败:', error); | |||||
| return this.json({ code: 0, data: { list: [], required: false } }); | |||||
| } | |||||
| } | |||||
| /** | /** | ||||
| * 获取省市区树形数据(一次返回) | * 获取省市区树形数据(一次返回) | ||||
| * GET /common/regions | * GET /common/regions | ||||
| @@ -355,6 +355,10 @@ module.exports = class extends Base { | |||||
| } | } | ||||
| let documents = []; | let documents = []; | ||||
| try { documents = JSON.parse(patient.documents || '[]'); } catch (e) { documents = []; } | try { documents = JSON.parse(patient.documents || '[]'); } catch (e) { documents = []; } | ||||
| let samplePhotos = []; | |||||
| try { samplePhotos = JSON.parse(patient.sample_photos || '[]'); } catch (e) { samplePhotos = []; } | |||||
| let sampleTypes = []; | |||||
| try { sampleTypes = JSON.parse(patient.sample_types || '[]'); } catch (e) { sampleTypes = []; } | |||||
| // 如果是驳回状态,查最近一条驳回原因 | // 如果是驳回状态,查最近一条驳回原因 | ||||
| let rejectReason = ''; | let rejectReason = ''; | ||||
| @@ -375,9 +379,21 @@ module.exports = class extends Base { | |||||
| city_name: regionMap[patient.city_code] || '', | city_name: regionMap[patient.city_code] || '', | ||||
| district_name: regionMap[patient.district_code] || '', | district_name: regionMap[patient.district_code] || '', | ||||
| address: patient.address || '', | address: patient.address || '', | ||||
| hospital: patient.hospital || '', | |||||
| emergency_contact: patient.emergency_contact || '', | emergency_contact: patient.emergency_contact || '', | ||||
| emergency_phone: patient.emergency_phone || '', | emergency_phone: patient.emergency_phone || '', | ||||
| tag: patient.tag || '', documents, | tag: patient.tag || '', documents, | ||||
| sample_types: sampleTypes, | |||||
| wax_return: patient.wax_return || 0, | |||||
| return_name: patient.return_name || '', | |||||
| return_phone: patient.return_phone || '', | |||||
| return_province_code: patient.return_province_code || '', | |||||
| return_city_code: patient.return_city_code || '', | |||||
| return_district_code: patient.return_district_code || '', | |||||
| return_address: patient.return_address || '', | |||||
| report_email: patient.report_email || '', | |||||
| sample_tracking_no: patient.sample_tracking_no || '', | |||||
| sample_photos: samplePhotos, | |||||
| sign_income: patient.sign_income || '', | sign_income: patient.sign_income || '', | ||||
| sign_privacy: patient.sign_privacy || '', | sign_privacy: patient.sign_privacy || '', | ||||
| sign_privacy_jhr: patient.sign_privacy_jhr || '', | sign_privacy_jhr: patient.sign_privacy_jhr || '', | ||||
| @@ -399,11 +415,12 @@ module.exports = class extends Base { | |||||
| async saveMyInfoAction() { | async saveMyInfoAction() { | ||||
| const mpUser = this.mpUser; | const mpUser = this.mpUser; | ||||
| if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); | if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); | ||||
| const { gender, province_code, city_code, district_code, address, emergency_contact, emergency_phone, documents, sign_income, sign_privacy, sign_privacy_jhr, sign_promise, income_amount, guardian_name, guardian_id_card, guardian_relation, mp_env_version } = this.post(); | |||||
| const { gender, province_code, city_code, district_code, address, hospital, emergency_contact, emergency_phone, documents, tag, 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, sign_income, sign_privacy, sign_privacy_jhr, sign_promise, income_amount, guardian_name, guardian_id_card, guardian_relation, mp_env_version } = this.post(); | |||||
| const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find(); | 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: '请先完成实名认证' }); | if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 1, msg: '请先完成实名认证' }); | ||||
| if (!province_code || !city_code || !district_code) return this.json({ code: 1, msg: '请选择省市区' }); | if (!province_code || !city_code || !district_code) return this.json({ code: 1, msg: '请选择省市区' }); | ||||
| if (!address) return this.json({ code: 1, msg: '请填写详细地址' }); | if (!address) return this.json({ code: 1, msg: '请填写详细地址' }); | ||||
| if (!emergency_contact || !emergency_phone) return this.json({ code: 1, msg: '请填写紧急联系人信息' }); | |||||
| const now = think.datetime(new Date()); | const now = think.datetime(new Date()); | ||||
| try { | try { | ||||
| @@ -414,8 +431,21 @@ module.exports = class extends Base { | |||||
| await this.model('patient').where({ id: user.patient_id }).update({ | await this.model('patient').where({ id: user.patient_id }).update({ | ||||
| gender: gender || '', province_code: province_code || '', city_code: city_code || '', | gender: gender || '', province_code: province_code || '', city_code: city_code || '', | ||||
| district_code: district_code || '', address: address || '', | district_code: district_code || '', address: address || '', | ||||
| hospital: hospital || '', | |||||
| emergency_contact: emergency_contact || '', emergency_phone: emergency_phone || '', | emergency_contact: emergency_contact || '', emergency_phone: emergency_phone || '', | ||||
| tag: tag || '', | |||||
| documents: JSON.stringify(documents || []), | documents: JSON.stringify(documents || []), | ||||
| sample_types: JSON.stringify(sample_types || []), | |||||
| wax_return: (sample_types && sample_types.length && wax_return) ? 1 : 0, | |||||
| return_name: (sample_types && sample_types.length && wax_return) ? (return_name || '') : '', | |||||
| return_phone: (sample_types && sample_types.length && wax_return) ? (return_phone || '') : '', | |||||
| return_province_code: (sample_types && sample_types.length && wax_return) ? (return_province_code || '') : '', | |||||
| return_city_code: (sample_types && sample_types.length && wax_return) ? (return_city_code || '') : '', | |||||
| return_district_code: (sample_types && sample_types.length && wax_return) ? (return_district_code || '') : '', | |||||
| return_address: (sample_types && sample_types.length && wax_return) ? (return_address || '') : '', | |||||
| report_email: (sample_types && sample_types.length) ? (report_email || '') : '', | |||||
| sample_tracking_no: (sample_types && sample_types.length) ? (sample_tracking_no || '') : '', | |||||
| sample_photos: (sample_types && sample_types.length) ? JSON.stringify(sample_photos || []) : '[]', | |||||
| sign_income: sign_income || '', | sign_income: sign_income || '', | ||||
| sign_privacy: sign_privacy || '', | sign_privacy: sign_privacy || '', | ||||
| sign_privacy_jhr: sign_privacy_jhr || '', | sign_privacy_jhr: sign_privacy_jhr || '', | ||||
| @@ -435,27 +465,6 @@ module.exports = class extends Base { | |||||
| operator_name: '用户自助提交' | operator_name: '用户自助提交' | ||||
| }); | }); | ||||
| // 异步 OCR 识别瘤种(不阻塞用户提交) | |||||
| const patientId = user.patient_id; | |||||
| const docUrls = documents || []; | |||||
| if (docUrls.length) { | |||||
| setImmediate(async () => { | |||||
| try { | |||||
| const tagOptions = await think.model('sys_config').getByKey('tag_options'); | |||||
| if (tagOptions && tagOptions.length) { | |||||
| const ocrService = think.service('ocr'); | |||||
| const matchedTag = await ocrService.matchTag(docUrls, tagOptions); | |||||
| if (matchedTag) { | |||||
| await think.model('patient').where({ id: patientId }).update({ tag: matchedTag }); | |||||
| think.logger.info(`[OCR] 患者(ID:${patientId}) 识别瘤种: ${matchedTag}`); | |||||
| } | |||||
| } | |||||
| } catch (err) { | |||||
| think.logger.error(`[OCR] 患者(ID:${patientId}) 识别失败:`, err.message); | |||||
| } | |||||
| }); | |||||
| } | |||||
| return this.json({ code: 0, data: {}, msg: '提交成功' }); | return this.json({ code: 0, data: {}, msg: '提交成功' }); | ||||
| } catch (error) { | } catch (error) { | ||||
| think.logger.error('saveMyInfo error:', error); | think.logger.error('saveMyInfo error:', error); | ||||
| @@ -0,0 +1,14 @@ | |||||
| module.exports = class extends think.Model { | |||||
| get tableName() { | |||||
| return 'sample_type'; | |||||
| } | |||||
| /** | |||||
| * 获取启用的样本类型列表 | |||||
| */ | |||||
| async getEnabledList() { | |||||
| return this.where({ status: 1, is_deleted: 0 }) | |||||
| .order('sort DESC, id ASC') | |||||
| .select(); | |||||
| } | |||||
| }; | |||||
| @@ -32,6 +32,16 @@ | |||||
| </div> | </div> | ||||
| {% endif %} | {% endif %} | ||||
| {# 送检样本管理 #} | |||||
| {% if isSuperAdmin or (userPermissions and 'tag' in userPermissions) %} | |||||
| <div class="menu-group"> | |||||
| <a class="menu-item {% if currentPage == 'sample-type' %}active{% endif %}" href="/admin/sample_type.html"> | |||||
| <span class="menu-icon"><svg viewBox="0 0 1024 1024" width="18" height="18"><path d="M320 128v768h384V128H320zm320 704H384V192h256v640z" fill="currentColor"/><path d="M448 320h128v64H448zm0 128h128v64H448zm0 128h96v64h-96z" fill="currentColor"/></svg></span> | |||||
| <span>送检样本管理</span> | |||||
| </a> | |||||
| </div> | |||||
| {% endif %} | |||||
| {# 内容管理 #} | {# 内容管理 #} | ||||
| {% if isSuperAdmin or (userPermissions and 'content' in userPermissions) %} | {% if isSuperAdmin or (userPermissions and 'content' in userPermissions) %} | ||||
| <div class="menu-group"> | <div class="menu-group"> | ||||
| @@ -13,6 +13,7 @@ | |||||
| .doc-images { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 12px; } | .doc-images { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 12px; } | ||||
| .sign-doc-card { width: 200px; border: 1px solid #EBEEF5; border-radius: 8px; padding: 16px; text-align: center; cursor: pointer; transition: all 0.2s; } | .sign-doc-card { width: 200px; border: 1px solid #EBEEF5; border-radius: 8px; padding: 16px; text-align: center; cursor: pointer; transition: all 0.2s; } | ||||
| .sign-doc-card:hover { border-color: var(--el-color-primary); box-shadow: 0 2px 12px rgba(255,120,0,0.15); } | .sign-doc-card:hover { border-color: var(--el-color-primary); box-shadow: 0 2px 12px rgba(255,120,0,0.15); } | ||||
| .sign-img-wrap .el-image__inner { object-position: center bottom !important; } | |||||
| .timeline-list { padding: 0; list-style: none; } | .timeline-list { padding: 0; list-style: none; } | ||||
| .timeline-list li { position: relative; padding: 0 0 20px 24px; border-left: 2px solid #EBEEF5; } | .timeline-list li { position: relative; padding: 0 0 20px 24px; border-left: 2px solid #EBEEF5; } | ||||
| .timeline-list li:last-child { border-left-color: transparent; padding-bottom: 0; } | .timeline-list li:last-child { border-left-color: transparent; padding-bottom: 0; } | ||||
| @@ -70,6 +71,45 @@ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <!-- 情况描述 --> | |||||
| <div class="detail-panel"> | |||||
| <h3>情况描述</h3> | |||||
| <div class="info-grid"> | |||||
| <div class="info-item"><span class="label">医院名称:</span><span class="value">${ patient.hospital || '—' }</span></div> | |||||
| <div class="info-item"><span class="label">瘤种:</span> | |||||
| <span class="value"> | |||||
| <el-tag v-if="patient.tag" type="danger" size="small">${ patient.tag }</el-tag> | |||||
| <span v-else style="color:#999;">未选择</span> | |||||
| </span> | |||||
| </div> | |||||
| <div class="info-item"><span class="label">送检样本:</span> | |||||
| <span class="value"> | |||||
| <template v-if="patient.sample_types && patient.sample_types.length"> | |||||
| <el-tag v-for="st in patient.sample_types" :key="st" size="small" style="margin-right:4px;">${ st }</el-tag> | |||||
| </template> | |||||
| <span v-else style="color:#999;">未选择</span> | |||||
| </span> | |||||
| </div> | |||||
| <div class="info-item" v-if="patient.wax_return"><span class="label">样本需寄回:</span><span class="value">是</span></div> | |||||
| <div class="info-item" v-if="patient.return_name"><span class="label">收件人:</span><span class="value">${ patient.return_name }</span></div> | |||||
| <div class="info-item" v-if="patient.return_phone"><span class="label">收件电话:</span><span class="value">${ patient.return_phone }</span></div> | |||||
| <div class="info-item" v-if="patient.return_address"><span class="label">收件地址:</span> | |||||
| <span class="value">${ patient.return_province_name || '' } ${ patient.return_city_name || '' } ${ patient.return_district_name || '' } ${ patient.return_address }</span> | |||||
| </div> | |||||
| <div class="info-item" v-if="patient.report_email"><span class="label">报告邮箱:</span><span class="value">${ patient.report_email }</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> | |||||
| <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 class="doc-images"> | |||||
| <div v-for="(photo, idx) in patient.sample_photos" :key="idx"> | |||||
| <el-image :src="photo" fit="cover" :preview-src-list="patient.sample_photos" :initial-index="idx" | |||||
| style="width:200px;height:140px;border-radius:8px;border:1px solid #EBEEF5;" /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <!-- 实名认证照片 --> | <!-- 实名认证照片 --> | ||||
| <div class="detail-panel" v-if="patient.id_card_front || patient.id_card_back || patient.photo"> | <div class="detail-panel" v-if="patient.id_card_front || patient.id_card_back || patient.photo"> | ||||
| <h3>实名认证照片</h3> | <h3>实名认证照片</h3> | ||||
| @@ -113,13 +153,13 @@ | |||||
| <!-- 签字材料 --> | <!-- 签字材料 --> | ||||
| <div class="detail-panel"> | <div class="detail-panel"> | ||||
| <h3>签字材料</h3> | <h3>签字材料</h3> | ||||
| <p style="font-size:13px;color:#909399;margin-bottom:16px;">患者签署的三份授权材料</p> | |||||
| <p style="font-size:13px;color:#909399;margin-bottom:16px;">患者签署的授权材料</p> | |||||
| <div style="display:flex;gap:20px;flex-wrap:wrap;"> | <div style="display:flex;gap:20px;flex-wrap:wrap;"> | ||||
| <div v-for="item in signDocs" :key="item.key" style="width:220px;"> | <div v-for="item in signDocs" :key="item.key" style="width:220px;"> | ||||
| <div style="font-size:13px;color:#303133;font-weight:500;margin-bottom:8px;">${ item.label }</div> | <div style="font-size:13px;color:#303133;font-weight:500;margin-bottom:8px;">${ item.label }</div> | ||||
| <template v-if="item.url && isImageUrl(item.url)"> | <template v-if="item.url && isImageUrl(item.url)"> | ||||
| <el-image :src="item.url" fit="cover" :preview-src-list="signImageList" :initial-index="signImageList.indexOf(item.url)" | <el-image :src="item.url" fit="cover" :preview-src-list="signImageList" :initial-index="signImageList.indexOf(item.url)" | ||||
| style="width:220px;height:160px;border-radius:8px;border:1px solid #EBEEF5;cursor:pointer;" /> | |||||
| class="sign-img-wrap" style="width:220px;height:160px;border-radius:8px;border:1px solid #EBEEF5;cursor:pointer;" /> | |||||
| </template> | </template> | ||||
| <template v-else-if="item.url"> | <template v-else-if="item.url"> | ||||
| <div class="sign-doc-card" @click="downloadSign(item.url, item.label)"> | <div class="sign-doc-card" @click="downloadSign(item.url, item.label)"> | ||||
| @@ -162,6 +202,7 @@ | |||||
| <!-- 驳回弹窗 --> | <!-- 驳回弹窗 --> | ||||
| <el-dialog v-model="rejectVisible" title="驳回审核" width="540px" destroy-on-close :close-on-click-modal="false" :z-index="2000"> | <el-dialog v-model="rejectVisible" title="驳回审核" width="540px" destroy-on-close :close-on-click-modal="false" :z-index="2000"> | ||||
| <p style="font-weight:500;color:#303133;margin-bottom:12px;">请选择或填写驳回原因(必填):</p> | <p style="font-weight:500;color:#303133;margin-bottom:12px;">请选择或填写驳回原因(必填):</p> | ||||
| <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;"> | <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;"> | ||||
| <el-check-tag v-for="(r, i) in commonReasons" :key="i" :checked="selectedReasons.includes(i)" | <el-check-tag v-for="(r, i) in commonReasons" :key="i" :checked="selectedReasons.includes(i)" | ||||
| @@ -173,8 +214,8 @@ | |||||
| <el-button type="danger" @click="doReject" :loading="rejectSaving">确认驳回</el-button> | <el-button type="danger" @click="doReject" :loading="rejectSaving">确认驳回</el-button> | ||||
| </div> | </div> | ||||
| </el-dialog> | </el-dialog> | ||||
| </div> | |||||
| {% endblock %} | |||||
| </div>{% endblock %} | |||||
| {% block js %} | {% block js %} | ||||
| <script> | <script> | ||||
| @@ -191,7 +232,14 @@ var app = createApp({ | |||||
| id: '', patient_no: '', name: '', phone: '', id_card: '', gender: '', birth_date: '', | id: '', patient_no: '', name: '', phone: '', id_card: '', gender: '', birth_date: '', | ||||
| province_code: '', city_code: '', district_code: '', | province_code: '', city_code: '', district_code: '', | ||||
| province_name: '', city_name: '', district_name: '', | province_name: '', city_name: '', district_name: '', | ||||
| address: '', tag: '', documents: [], sign_income: '', sign_privacy: '', sign_promise: '', | |||||
| address: '', hospital: '', tag: '', documents: [], | |||||
| sample_types: [], wax_return: 0, | |||||
| return_name: '', return_phone: '', | |||||
| return_province_code: '', return_city_code: '', return_district_code: '', | |||||
| return_province_name: '', return_city_name: '', return_district_name: '', | |||||
| return_address: '', | |||||
| report_email: '', sample_tracking_no: '', sample_photos: [], | |||||
| sign_income: '', sign_privacy: '', sign_promise: '', | |||||
| emergency_contact: '', emergency_phone: '', | emergency_contact: '', emergency_phone: '', | ||||
| status: -1, create_time: '' | status: -1, create_time: '' | ||||
| }); | }); | ||||
| @@ -199,8 +247,7 @@ var app = createApp({ | |||||
| var rejectVisible = ref(false); | var rejectVisible = ref(false); | ||||
| var rejectSaving = ref(false); | var rejectSaving = ref(false); | ||||
| var rejectReason = ref(''); | var rejectReason = ref(''); | ||||
| var selectedReasons = ref([]); | |||||
| var commonReasons = [ | |||||
| var selectedReasons = ref([]); var commonReasons = [ | |||||
| '身份证照片模糊,请重新上传', | '身份证照片模糊,请重新上传', | ||||
| '病历资料不完整,请补充', | '病历资料不完整,请补充', | ||||
| '检查报告缺失,请上传', | '检查报告缺失,请上传', | ||||
| @@ -0,0 +1,199 @@ | |||||
| {% extends "./layout.html" %} | |||||
| {% block title %}送检样本管理{% endblock %} | |||||
| {% block css %} | |||||
| <style> | |||||
| .drag-handle { cursor: move; color: #999; font-size: 18px; } | |||||
| .drag-handle:hover { color: #ff7800; } | |||||
| .sortable-ghost { opacity: 0.4; background: #fff7ed; } | |||||
| </style> | |||||
| {% endblock %} | |||||
| {% block content %} | |||||
| <div id="sampleTypeApp" v-cloak> | |||||
| <el-card shadow="never"> | |||||
| <template #header> | |||||
| <div class="flex items-center justify-between"> | |||||
| <div class="flex items-center gap-4"> | |||||
| <el-button type="primary" @click="showAdd">+ 新增样本类型</el-button> | |||||
| <el-switch v-model="sampleRequired" active-text="送检样本必选" inactive-text="送检样本非必选" | |||||
| @change="handleRequiredChange" /> | |||||
| </div> | |||||
| <span class="text-xs text-gray-400">拖拽行可调整排序</span> | |||||
| </div> | |||||
| </template> | |||||
| <el-table :data="list" v-loading="loading" stripe border row-key="id"> | |||||
| <el-table-column width="60" align="center"> | |||||
| <template #header>排序</template> | |||||
| <template #default> | |||||
| <span class="drag-handle">⠿</span> | |||||
| </template> | |||||
| </el-table-column> | |||||
| <el-table-column prop="id" label="ID" width="70"></el-table-column> | |||||
| <el-table-column prop="name" label="样本类型名称"></el-table-column> | |||||
| <el-table-column label="可选需寄回" width="120" align="center"> | |||||
| <template #default="{ row }"> | |||||
| <el-tag :type="row.need_return ? 'success' : 'info'" size="small">${ row.need_return ? '是' : '否' }</el-tag> | |||||
| </template> | |||||
| </el-table-column> | |||||
| <el-table-column label="状态" width="100" align="center"> | |||||
| <template #default="{ row }"> | |||||
| <el-tag :type="row.status ? 'success' : 'danger'" size="small">${ row.status ? '启用' : '禁用' }</el-tag> | |||||
| </template> | |||||
| </el-table-column> | |||||
| <el-table-column label="操作" width="180" align="center"> | |||||
| <template #default="{ row }"> | |||||
| <el-button type="primary" link @click="showEdit(row)">编辑</el-button> | |||||
| <el-button type="danger" link @click="handleDelete(row)">删除</el-button> | |||||
| </template> | |||||
| </el-table-column> | |||||
| </el-table> | |||||
| </el-card> | |||||
| <!-- 新增/编辑弹窗 --> | |||||
| <el-dialog v-model="dialogVisible" :title="dialogTitle" width="480px" destroy-on-close draggable :close-on-click-modal="false"> | |||||
| <el-form :model="form" label-width="110px"> | |||||
| <el-form-item label="样本类型名称" required> | |||||
| <el-input v-model="form.name" placeholder="请输入样本类型名称" maxlength="50" /> | |||||
| </el-form-item> | |||||
| <el-form-item label="可选需寄回"> | |||||
| <el-switch v-model="form.need_return" /> | |||||
| <span style="margin-left:8px;font-size:12px;color:#909399;">开启后,用户选择该样本类型时可选择是否需要寄回</span> | |||||
| </el-form-item> | |||||
| <el-form-item label="排序"> | |||||
| <el-input-number v-model="form.sort" :min="0" :max="999" /> | |||||
| </el-form-item> | |||||
| <el-form-item label="状态"> | |||||
| <el-switch v-model="form.status" :active-value="1" :inactive-value="0" /> | |||||
| </el-form-item> | |||||
| </el-form> | |||||
| <template #footer> | |||||
| <el-button @click="dialogVisible = false">取消</el-button> | |||||
| <el-button type="primary" @click="handleSave" :loading="saving">确定</el-button> | |||||
| </template> | |||||
| </el-dialog> | |||||
| </div> | |||||
| {% endblock %} | |||||
| {% block js %} | |||||
| <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script> | |||||
| <script> | |||||
| const initRequired = '{{ sampleRequired }}'; | |||||
| const { createApp, ref, reactive, onMounted, nextTick } = Vue; | |||||
| const app = createApp({ | |||||
| delimiters: ['${', '}'], | |||||
| setup() { | |||||
| const loading = ref(false); | |||||
| const list = ref([]); | |||||
| const sampleRequired = ref(initRequired === '1'); | |||||
| const dialogVisible = ref(false); | |||||
| const dialogTitle = ref('新增样本类型'); | |||||
| const saving = ref(false); | |||||
| const form = reactive({ id: null, name: '', need_return: false, sort: 0, status: 1 }); | |||||
| let sortableInstance = null; | |||||
| async function loadList() { | |||||
| loading.value = true; | |||||
| try { | |||||
| const res = await fetch('/admin/sample_type/list').then(r => r.json()); | |||||
| if (res.code === 0) { | |||||
| list.value = res.data || []; | |||||
| nextTick(() => initSortable()); | |||||
| } | |||||
| } finally { loading.value = false; } | |||||
| } | |||||
| function initSortable() { | |||||
| if (sortableInstance) { sortableInstance.destroy(); sortableInstance = null; } | |||||
| const tbody = document.querySelector('#sampleTypeApp .el-table__body-wrapper tbody'); | |||||
| if (!tbody) return; | |||||
| sortableInstance = Sortable.create(tbody, { | |||||
| handle: '.drag-handle', | |||||
| animation: 150, | |||||
| ghostClass: 'sortable-ghost', | |||||
| onEnd(evt) { | |||||
| if (evt.oldIndex === evt.newIndex) return; | |||||
| const arr = list.value.slice(); | |||||
| const item = arr.splice(evt.oldIndex, 1)[0]; | |||||
| arr.splice(evt.newIndex, 0, item); | |||||
| list.value = arr; | |||||
| fetch('/admin/sample_type/sort', { | |||||
| method: 'POST', | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify({ ids: arr.map(i => i.id) }) | |||||
| }).then(r => r.json()).then(res => { | |||||
| if (res.code === 0) ElementPlus.ElMessage.success('排序已保存'); | |||||
| else loadList(); | |||||
| }); | |||||
| } | |||||
| }); | |||||
| } | |||||
| function showAdd() { | |||||
| dialogTitle.value = '新增样本类型'; | |||||
| form.id = null; form.name = ''; form.need_return = false; form.sort = 0; form.status = 1; | |||||
| dialogVisible.value = true; | |||||
| } | |||||
| function showEdit(row) { | |||||
| dialogTitle.value = '编辑样本类型'; | |||||
| form.id = row.id; form.name = row.name; form.need_return = !!row.need_return; form.sort = row.sort; form.status = row.status; | |||||
| dialogVisible.value = true; | |||||
| } | |||||
| async function handleSave() { | |||||
| if (!form.name.trim()) { ElementPlus.ElMessage.warning('请输入样本类型名称'); return; } | |||||
| saving.value = true; | |||||
| try { | |||||
| const url = form.id ? '/admin/sample_type/edit' : '/admin/sample_type/add'; | |||||
| const res = await fetch(url, { | |||||
| method: 'POST', | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify(form) | |||||
| }).then(r => r.json()); | |||||
| if (res.code === 0) { | |||||
| ElementPlus.ElMessage.success('保存成功'); | |||||
| dialogVisible.value = false; | |||||
| loadList(); | |||||
| } else { | |||||
| ElementPlus.ElMessage.error(res.msg || '保存失败'); | |||||
| } | |||||
| } finally { saving.value = false; } | |||||
| } | |||||
| async function handleDelete(row) { | |||||
| try { | |||||
| await ElementPlus.ElMessageBox.confirm(`确定要删除「${row.name}」吗?`, '提示', { type: 'warning' }); | |||||
| const res = await fetch('/admin/sample_type/delete', { | |||||
| method: 'POST', | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify({ id: row.id }) | |||||
| }).then(r => r.json()); | |||||
| if (res.code === 0) { ElementPlus.ElMessage.success('删除成功'); loadList(); } | |||||
| else { ElementPlus.ElMessage.error(res.msg || '删除失败'); } | |||||
| } catch (e) {} | |||||
| } | |||||
| async function handleRequiredChange(val) { | |||||
| const res = await fetch('/admin/sample_type/setRequired', { | |||||
| method: 'POST', | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify({ value: val ? 1 : 0 }) | |||||
| }).then(r => r.json()); | |||||
| if (res.code === 0) ElementPlus.ElMessage.success('设置已保存'); | |||||
| else ElementPlus.ElMessage.error(res.msg || '设置失败'); | |||||
| } | |||||
| onMounted(() => loadList()); | |||||
| return { loading, list, sampleRequired, dialogVisible, dialogTitle, saving, form, showAdd, showEdit, handleSave, handleDelete, handleRequiredChange }; | |||||
| } | |||||
| }); | |||||
| app.use(ElementPlus, { locale: ElementPlusLocaleZhCn }); | |||||
| app.mount('#sampleTypeApp'); | |||||
| </script> | |||||
| {% endblock %} | |||||