Преглед изворни кода

feat:修复一系列BUG

master
leiyun пре 2 дана
родитељ
комит
60aaa9fd6d
6 измењених фајлова са 147 додато и 22 уклоњено
  1. +5
    -0
      sql/guardian_fields.sql
  2. +20
    -7
      src/controller/admin/patient.js
  3. +7
    -1
      src/controller/mp.js
  4. +7
    -1
      src/middleware/request_log.js
  5. +41
    -3
      view/admin/patient_detail.html
  6. +67
    -10
      view/admin/patient_index.html

+ 5
- 0
sql/guardian_fields.sql Прегледај датотеку

@@ -0,0 +1,5 @@
-- 患者表增加监护人信息字段
ALTER TABLE `patient`
ADD COLUMN `guardian_name` varchar(50) NOT NULL DEFAULT '' COMMENT '监护人姓名' AFTER `sign_privacy_jhr`,
ADD COLUMN `guardian_id_card` varchar(18) NOT NULL DEFAULT '' COMMENT '监护人身份证号' AFTER `guardian_name`,
ADD COLUMN `guardian_relation` varchar(20) NOT NULL DEFAULT '' COMMENT '与患者关系' AFTER `guardian_id_card`;

+ 20
- 7
src/controller/admin/patient.js Прегледај датотеку

@@ -1,4 +1,5 @@
const Base = require('../base.js');
const dayjs = require('dayjs');

module.exports = class extends Base {
// 患者列表页面
@@ -122,7 +123,7 @@ module.exports = class extends Base {
// 新增患者
async addAction() {
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 } = data;
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;

if (!name || !phone || !id_card || !gender || !birth_date) {
return this.fail('请填写完整信息');
@@ -165,14 +166,19 @@ module.exports = class extends Base {
sign_income: sign_income || '',
sign_privacy: sign_privacy || '',
sign_promise: sign_promise || '',
status: 0,
sign_privacy_jhr: sign_privacy_jhr || '',
income_amount: income_amount || '',
guardian_name: guardian_name || '',
guardian_id_card: guardian_id_card || '',
guardian_relation: guardian_relation || '',
status: 1,
create_by: this.adminUser?.id || 0
});

// 记录审核日志
// 记录审核日志(后台新增直接审核通过)
await this.model('patient_audit').add({
patient_id: id,
action: 'submit',
action: 'approve',
operator_id: this.adminUser?.id || 0,
operator_name: this.adminUser?.nickname || this.adminUser?.username || ''
});
@@ -272,7 +278,7 @@ module.exports = class extends Base {
// 编辑患者
async editAction() {
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 } = data;
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;

if (!id) return this.fail('参数错误');
if (!name || !phone || !id_card || !gender || !birth_date) return this.fail('请填写完整信息');
@@ -295,6 +301,11 @@ module.exports = class extends Base {
sign_income: sign_income || '',
sign_privacy: sign_privacy || '',
sign_promise: sign_promise || '',
sign_privacy_jhr: sign_privacy_jhr || '',
income_amount: income_amount || '',
guardian_name: guardian_name || '',
guardian_id_card: guardian_id_card || '',
guardian_relation: guardian_relation || '',
update_by: this.adminUser?.id || 0
});

@@ -337,7 +348,7 @@ module.exports = class extends Base {
}

const statusMap = { '-1': '待提交', 0: '待审核', 1: '审核通过', 2: '已驳回' };
const header = ['ID', '姓名', '性别', '身份证', '手机号', '省份', '城市', '审核状态', '审核日期', '审核驳回原因', '瘤种'];
const header = ['ID', '姓名', '性别', '身份证', '手机号', '省份', '城市', '提交时间', '审核状态', '审核日期', '审核驳回原因', '瘤种'];

const ExcelJS = require('exceljs');
const workbook = new ExcelJS.Workbook();
@@ -379,6 +390,7 @@ module.exports = class extends Base {
item.phone,
regionMap[item.province_code] || '',
regionMap[item.city_code] || '',
item.create_time || '',
statusMap[item.status] || '',
audit ? (audit.create_time || '') : '',
(audit && audit.action === 'reject') ? (audit.reason || '') : '',
@@ -401,7 +413,8 @@ module.exports = class extends Base {
});

const buffer = await workbook.xlsx.writeBuffer();
const fileName = encodeURIComponent(`患者信息_${Date.now()}.xlsx`);
const ts = dayjs().format('YYYYMMDDHHmmss');
const fileName = encodeURIComponent(`患者信息_${ts}.xlsx`);

this.ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
this.ctx.set('Content-Disposition', `attachment; filename*=UTF-8''${fileName}`);


+ 7
- 1
src/controller/mp.js Прегледај датотеку

@@ -306,6 +306,9 @@ module.exports = class extends Base {
sign_privacy_jhr: patient.sign_privacy_jhr || '',
sign_promise: patient.sign_promise || '',
income_amount: patient.income_amount || '',
guardian_name: patient.guardian_name || '',
guardian_id_card: patient.guardian_id_card || '',
guardian_relation: patient.guardian_relation || '',
status: patient.status, auth_status: patient.auth_status || 0,
reject_reason: rejectReason
}});
@@ -319,7 +322,7 @@ module.exports = class extends Base {
async saveMyInfoAction() {
const mpUser = this.mpUser;
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, mp_env_version } = this.post();
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 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 (!province_code || !city_code || !district_code) return this.json({ code: 1, msg: '请选择省市区' });
@@ -341,6 +344,9 @@ module.exports = class extends Base {
sign_privacy_jhr: sign_privacy_jhr || '',
sign_promise: sign_promise || '',
income_amount: income_amount || null,
guardian_name: guardian_name || '',
guardian_id_card: guardian_id_card || '',
guardian_relation: guardian_relation || '',
status: 0,
update_time: now
});


+ 7
- 1
src/middleware/request_log.js Прегледај датотеку

@@ -3,9 +3,15 @@ const dayjs = require('dayjs');
module.exports = () => {
return async (ctx, next) => {
// 跳过静态资源和source map
if (ctx.path.startsWith('/static/') || ctx.path === '/favicon.ico' || ctx.path.endsWith('.map')) {
if (ctx.path.startsWith('/static/') || ctx.path === '/favicon.ico') {
return next();
}
// source map / json 静态请求直接返回 404,避免 think-trace 报错
if (ctx.path.endsWith('.map') || ctx.path.endsWith('.json')) {
ctx.status = 404;
ctx.body = '';
return;
}

const start = Date.now();
const params = { ...ctx.query, ...(ctx.request.body || {}) };


+ 41
- 3
view/admin/patient_detail.html Прегледај датотеку

@@ -63,6 +63,36 @@
<div class="info-item"><span class="label">提交时间:</span><span class="value">${ patient.create_time }</span></div>
<div class="info-item"><span class="label">紧急联系人:</span><span class="value">${ patient.emergency_contact || '—' }</span></div>
<div class="info-item"><span class="label">紧急联系电话:</span><span class="value">${ patient.emergency_phone || '—' }</span></div>
<div class="info-item" v-if="patient.income_amount"><span class="label">年可支配收入:</span><span class="value">${ patient.income_amount } 元</span></div>
<div class="info-item" v-if="patient.guardian_name"><span class="label">监护人姓名:</span><span class="value">${ patient.guardian_name }</span></div>
<div class="info-item" v-if="patient.guardian_id_card"><span class="label">监护人身份证:</span><span class="value">${ patient.guardian_id_card }</span></div>
<div class="info-item" v-if="patient.guardian_relation"><span class="label">与患者关系:</span><span class="value">${ patient.guardian_relation }</span></div>
</div>
</div>

<!-- 实名认证照片 -->
<div class="detail-panel" v-if="patient.id_card_front || patient.id_card_back || patient.photo">
<h3>实名认证照片</h3>
<p style="font-size:13px;color:#909399;margin-bottom:12px;">
<template v-if="patient.id_card_type === 2">无证件儿童免冠照片</template>
<template v-else>身份证人像面 / 国徽面照片</template>
</p>
<div class="doc-images">
<div v-if="patient.id_card_front">
<el-image :src="patient.id_card_front" fit="cover" :preview-src-list="authImageList"
:initial-index="0" style="width:260px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
<div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">人像面</div>
</div>
<div v-if="patient.id_card_back">
<el-image :src="patient.id_card_back" fit="cover" :preview-src-list="authImageList"
:initial-index="patient.id_card_front ? 1 : 0" style="width:260px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
<div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">国徽面</div>
</div>
<div v-if="patient.photo">
<el-image :src="patient.photo" fit="cover" :preview-src-list="[patient.photo]"
:initial-index="0" style="width:170px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
<div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">免冠照片</div>
</div>
</div>
</div>

@@ -212,15 +242,23 @@ var app = createApp({
}

var signDocs = Vue.computed(function() {
return [
var docs = [
{ key: 'income', label: '个人可支配收入声明', url: patient.sign_income },
{ key: 'privacy', label: '个人信息处理同意书', url: patient.sign_privacy },
{ key: 'promise', label: '声明与承诺', url: patient.sign_promise }
];
if (patient.sign_privacy_jhr) {
docs.push({ key: 'privacy_jhr', label: '监护人个人信息处理同意书', url: patient.sign_privacy_jhr });
}
return docs;
});

var signImageList = Vue.computed(function() {
return [patient.sign_income, patient.sign_privacy, patient.sign_promise].filter(function(u) { return u && isImageUrl(u); });
return [patient.sign_income, patient.sign_privacy, patient.sign_promise, patient.sign_privacy_jhr].filter(function(u) { return u && isImageUrl(u); });
});

var authImageList = Vue.computed(function() {
return [patient.id_card_front, patient.id_card_back, patient.photo].filter(Boolean);
});

async function handleApprove() {
@@ -283,7 +321,7 @@ var app = createApp({
return {
loading, patient, audits, canAudit,
rejectVisible, rejectSaving, rejectReason, selectedReasons, commonReasons,
goBack, downloadSign, isImageUrl, signDocs, signImageList, handleApprove, showRejectDialog, toggleReason, doReject
goBack, downloadSign, isImageUrl, signDocs, signImageList, authImageList, handleApprove, showRejectDialog, toggleReason, doReject
};
}
});


+ 67
- 10
view/admin/patient_index.html Прегледај датотеку

@@ -170,14 +170,38 @@
<el-input v-model="addForm.emergency_phone" placeholder="联系人电话" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年可支配收入(元)">
<el-input v-model="addForm.income_amount" placeholder="请输入年可支配收入" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="isMinorComputed">
<el-form-item label="监护人姓名">
<el-input v-model="addForm.guardian_name" placeholder="请输入监护人姓名" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="isMinorComputed">
<el-form-item label="监护人身份证">
<el-input v-model="addForm.guardian_id_card" placeholder="请输入监护人身份证号" maxlength="18" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="isMinorComputed">
<el-form-item label="与患者关系">
<el-select v-model="addForm.guardian_relation" placeholder="请选择" style="width:100%;">
<el-option label="父亲" value="父亲"></el-option>
<el-option label="母亲" value="母亲"></el-option>
<el-option label="其他" value="其他"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="上传资料">
<div class="flex flex-wrap gap-2">
<div v-for="(doc, idx) in addForm.documents" :key="idx"
class="relative" style="width:80px;height:80px;">
<el-image :src="doc" fit="cover" style="width:80px;height:80px;border-radius:6px;border:1px solid #eee;" />
<span @click="addForm.documents.splice(idx, 1)"
class="absolute top-0 right-0 cursor-pointer bg-black/50 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">×</span>
style="position:relative;width:80px;height:80px;">
<el-image :src="doc" fit="cover" style="width:80px;height:80px;border-radius:6px;border:1px solid #eee;"></el-image>
<div @click="addForm.documents.splice(idx, 1)"
style="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;">×</div>
</div>
<el-upload action="/admin/upload" :show-file-list="false" accept="image/*"
:on-success="onDocUpload" :headers="uploadHeaders" style="width:80px;height:80px;">
@@ -208,6 +232,12 @@
${ addForm.sign_promise ? '✅' : '📄' } 声明与承诺
</el-button>
</el-upload>
<el-upload v-if="isMinorComputed" action="/admin/upload" :show-file-list="false" accept=".pdf,.png,.jpg,.jpeg"
:on-success="(res) => onSignUpload(res, 'sign_privacy_jhr')" :headers="uploadHeaders">
<el-button :type="addForm.sign_privacy_jhr ? 'success' : 'default'" plain>
${ addForm.sign_privacy_jhr ? '✅' : '📄' } 监护人个人信息处理同意书
</el-button>
</el-upload>
</div>
</el-form-item>
</el-col>
@@ -223,7 +253,7 @@

{% block js %}
<script>
const { createApp, ref, reactive, onMounted } = Vue;
const { createApp, ref, reactive, onMounted, computed } = Vue;
const { Plus, Download } = ElementPlusIconsVue;

const perms = {
@@ -254,7 +284,8 @@ const app = createApp({
const addForm = reactive({
name: '', phone: '', id_card: '', gender: '', birth_date: '',
regionCodes: [], address: '', emergency_contact: '', emergency_phone: '',
tag: '', documents: [], sign_income: '', sign_privacy: '', sign_promise: ''
tag: '', documents: [], sign_income: '', sign_privacy: '', sign_promise: '',
sign_privacy_jhr: '', income_amount: '', guardian_name: '', guardian_id_card: '', guardian_relation: ''
});

// 省市区树形数据
@@ -263,6 +294,20 @@ const app = createApp({
// 瘤种选项(从接口加载)
const tagOptions = ref([]);

// 判断是否未成年(根据身份证号计算年龄)
const isMinorComputed = computed(function() {
var idCard = addForm.id_card;
if (!idCard || idCard.length !== 18) return false;
var birthStr = idCard.substring(6, 10) + '-' + idCard.substring(10, 12) + '-' + idCard.substring(12, 14);
var birth = new Date(birthStr);
if (isNaN(birth.getTime())) return false;
var now = new Date();
var age = now.getFullYear() - birth.getFullYear();
var m = now.getMonth() - birth.getMonth();
if (m < 0 || (m === 0 && now.getDate() < birth.getDate())) age--;
return age < 18;
});

const regionFilter = ref([]);
const exporting = ref(false);

@@ -345,7 +390,8 @@ const app = createApp({
Object.assign(addForm, {
name: '', phone: '', id_card: '', gender: '', birth_date: '',
regionCodes: [], address: '', emergency_contact: '', emergency_phone: '',
tag: '', documents: [], sign_income: '', sign_privacy: '', sign_promise: ''
tag: '', documents: [], sign_income: '', sign_privacy: '', sign_promise: '',
sign_privacy_jhr: '', income_amount: '', guardian_name: '', guardian_id_card: '', guardian_relation: ''
});
addVisible.value = true;
}
@@ -380,7 +426,8 @@ const app = createApp({
Object.assign(addForm, {
name: '', phone: '', id_card: '', gender: '', birth_date: '',
regionCodes: [], address: '', emergency_contact: '', emergency_phone: '',
tag: '', documents: [], sign_income: '', sign_privacy: '', sign_promise: ''
tag: '', documents: [], sign_income: '', sign_privacy: '', sign_promise: '',
sign_privacy_jhr: '', income_amount: '', guardian_name: '', guardian_id_card: '', guardian_relation: ''
});
try {
var res = await fetch('/admin/patient/info?id=' + row.id).then(function(r) { return r.json(); });
@@ -400,6 +447,11 @@ const app = createApp({
sign_income: p.sign_income || '',
sign_privacy: p.sign_privacy || '',
sign_promise: p.sign_promise || '',
sign_privacy_jhr: p.sign_privacy_jhr || '',
income_amount: p.income_amount || '',
guardian_name: p.guardian_name || '',
guardian_id_card: p.guardian_id_card || '',
guardian_relation: p.guardian_relation || '',
regionCodes: [p.province_code, p.city_code, p.district_code].filter(Boolean)
});
addVisible.value = true;
@@ -439,7 +491,12 @@ const app = createApp({
documents: addForm.documents,
sign_income: addForm.sign_income,
sign_privacy: addForm.sign_privacy,
sign_promise: addForm.sign_promise
sign_promise: addForm.sign_promise,
sign_privacy_jhr: addForm.sign_privacy_jhr || '',
income_amount: addForm.income_amount || '',
guardian_name: addForm.guardian_name || '',
guardian_id_card: addForm.guardian_id_card || '',
guardian_relation: addForm.guardian_relation || ''
};
if (editingId.value) body.id = editingId.value;

@@ -486,7 +543,7 @@ const app = createApp({
return {
keyword, dateRange, tagFilter, regionFilter, activeTab, loading, tableData, pagination, counts,
uploadHeaders, addVisible, addSaving, addForm, exporting, editingId, perms,
regionTree, tagOptions, Plus, Download,
regionTree, tagOptions, isMinorComputed, Plus, Download,
loadList, resetFilter, onTabChange, onSizeChange, viewDetail, showAddDialog, showEditDialog, handleExport,
onIdCardInput, onDocUpload, onSignUpload, submitAdd
};


Loading…
Откажи
Сачувај