|
- {% extends "./layout.html" %}
-
- {% block title %}患者管理{% endblock %}
-
- {% block content %}
- <div id="patientApp" v-cloak>
- <!-- 搜索栏 -->
- <el-card shadow="never" class="mb-4">
- <el-form :inline="true" @submit.prevent="loadList()" class="flex items-center flex-wrap gap-2">
- <el-form-item label="关键词" class="!mb-0">
- <el-input v-model="keyword" placeholder="姓名/手机号/编号" clearable style="width:180px;" />
- </el-form-item>
- <el-form-item label="日期范围" class="!mb-0">
- <el-date-picker v-model="dateRange" type="daterange" range-separator="至"
- start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD"
- style="width:260px;" />
- </el-form-item>
- <el-form-item label="瘤种" class="!mb-0">
- <el-select v-model="tagFilter" placeholder="全部瘤种" clearable filterable style="width:160px;">
- <el-option v-for="t in tagOptions" :key="t" :label="t" :value="t"></el-option>
- <el-option label="无瘤种" value="none"></el-option>
- </el-select>
- </el-form-item>
- <el-form-item label="地区" class="!mb-0">
- <el-cascader v-model="regionFilter" :options="regionTree"
- :props="{ value: 'code', label: 'name', children: 'children', checkStrictly: true }"
- placeholder="全部地区" clearable style="width:220px;" />
- </el-form-item>
- <el-form-item class="!mb-0">
- <el-button type="primary" @click="loadList()">搜索</el-button>
- <el-button @click="resetFilter">重置</el-button>
- </el-form-item>
- </el-form>
- <div style="display:flex;gap:8px;margin-top:12px;">
- <el-button v-if="perms.canAdd" type="success" :icon="Plus" @click="showAddDialog">新增患者</el-button>
- <el-button v-if="perms.canExport" :icon="Download" @click="handleExport" :loading="exporting">导出</el-button>
- </div>
- </el-card>
-
- <!-- 状态 Tab -->
- <el-card shadow="never">
- <el-tabs v-model="activeTab" @tab-change="onTabChange">
- <el-tab-pane name="all">
- <template #label>全部 <el-badge :value="counts.all" type="info" class="ml-1" /></template>
- </el-tab-pane>
- <el-tab-pane name="draft">
- <template #label>待提交 <el-badge :value="counts.draft" class="ml-1" /></template>
- </el-tab-pane>
- <el-tab-pane name="pending">
- <template #label>待审核 <el-badge :value="counts.pending" type="warning" class="ml-1" /></template>
- </el-tab-pane>
- <el-tab-pane name="rejected">
- <template #label>已驳回 <el-badge :value="counts.rejected" type="danger" class="ml-1" /></template>
- </el-tab-pane>
- <el-tab-pane name="approved">
- <template #label>审核通过 <el-badge :value="counts.approved" type="success" class="ml-1" /></template>
- </el-tab-pane>
- </el-tabs>
-
- <el-table :data="tableData" v-loading="loading" stripe border>
- <el-table-column prop="patient_no" label="编号" min-width="200"></el-table-column>
- <el-table-column prop="name" label="姓名" min-width="80"></el-table-column>
- <el-table-column label="身份证号" min-width="180">
- <template #default="{ row }">${ row.id_card_mask }</template>
- </el-table-column>
- <el-table-column label="手机号" min-width="130">
- <template #default="{ row }">${ row.phone_mask }</template>
- </el-table-column>
- <el-table-column label="地区" min-width="160">
- <template #default="{ row }">${ row.region_name || '—' }</template>
- </el-table-column>
- <el-table-column label="瘤种" min-width="120">
- <template #default="{ row }">
- <el-tag v-if="row.tag" type="danger" size="small">${ row.tag }</el-tag>
- <span v-else style="color:#999;">—</span>
- </template>
- </el-table-column>
- <el-table-column label="提交时间" min-width="170">
- <template #default="{ row }">${ row.create_time }</template>
- </el-table-column>
- <el-table-column label="状态" min-width="100" align="center">
- <template #default="{ row }">
- <el-tag v-if="row.status === -1" type="info" size="small">待提交</el-tag>
- <el-tag v-else-if="row.status === 0" type="warning" size="small">待审核</el-tag>
- <el-tag v-else-if="row.status === 1" type="success" size="small">审核通过</el-tag>
- <el-tag v-else-if="row.status === 2" type="danger" size="small">已驳回</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="200" 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>
- </template>
- </el-table-column>
- </el-table>
-
- <div class="flex justify-end mt-4">
- <el-pagination
- v-model:current-page="pagination.page"
- v-model:page-size="pagination.pageSize"
- :page-sizes="[10, 20, 50, 100]"
- :total="pagination.total"
- layout="total, sizes, prev, pager, next"
- @current-change="loadList"
- @size-change="onSizeChange"
- />
- </div>
- </el-card>
-
- <!-- 新增患者弹窗 -->
- <el-dialog v-model="addVisible" :title="editingId ? '编辑患者' : '新增患者'" width="720px" destroy-on-close draggable :close-on-click-modal="false">
- <el-form :model="addForm" label-width="120px">
- <el-row :gutter="16">
- <el-col :span="12">
- <el-form-item label="姓名" required>
- <el-input v-model="addForm.name" placeholder="请输入姓名" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="瘤种">
- <el-select v-model="addForm.tag" placeholder="请选择或输入瘤种" clearable filterable allow-create style="width:100%;">
- <el-option v-for="t in tagOptions" :key="t" :label="t" :value="t"></el-option>
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="身份证号" required>
- <el-input v-model="addForm.id_card" placeholder="请输入身份证号" maxlength="18" @input="onIdCardInput" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="手机号" required>
- <el-input v-model="addForm.phone" placeholder="请输入手机号" maxlength="11" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="出生日期" required>
- <el-date-picker v-model="addForm.birth_date" type="date" placeholder="选择日期"
- value-format="YYYY-MM-DD" style="width:100%;" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="性别" required>
- <el-select v-model="addForm.gender" placeholder="请选择" style="width:100%;">
- <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="所在地区" required>
- <el-cascader v-model="addForm.regionCodes" :options="regionTree"
- :props="{ value: 'code', label: 'name', children: 'children' }"
- placeholder="请选择省/市/区" clearable style="width:100%;" />
- </el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="详细地址" required>
- <el-input v-model="addForm.address" placeholder="请输入详细地址(街道门牌号等)" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="紧急联系人">
- <el-input v-model="addForm.emergency_contact" placeholder="联系人姓名" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="紧急联系电话">
- <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"
- 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;">
- <div class="flex items-center justify-center border-2 border-dashed border-gray-300 rounded-md cursor-pointer text-2xl text-gray-400"
- style="width:80px;height:80px;">+</div>
- </el-upload>
- </div>
- </el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="签字材料">
- <div class="flex flex-wrap gap-3">
- <el-upload action="/admin/upload" :show-file-list="false" accept=".pdf,.png,.jpg,.jpeg"
- :on-success="(res) => onSignUpload(res, 'sign_income')" :headers="uploadHeaders">
- <el-button :type="addForm.sign_income ? 'success' : 'default'" plain>
- ${ addForm.sign_income ? '✅' : '📄' } 个人可支配收入声明
- </el-button>
- </el-upload>
- <el-upload action="/admin/upload" :show-file-list="false" accept=".pdf,.png,.jpg,.jpeg"
- :on-success="(res) => onSignUpload(res, 'sign_privacy')" :headers="uploadHeaders">
- <el-button :type="addForm.sign_privacy ? 'success' : 'default'" plain>
- ${ addForm.sign_privacy ? '✅' : '📄' } 个人信息处理同意书
- </el-button>
- </el-upload>
- <el-upload action="/admin/upload" :show-file-list="false" accept=".pdf,.png,.jpg,.jpeg"
- :on-success="(res) => onSignUpload(res, 'sign_promise')" :headers="uploadHeaders">
- <el-button :type="addForm.sign_promise ? 'success' : 'default'" plain>
- ${ 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>
- </el-row>
- </el-form>
- <template #footer>
- <el-button @click="addVisible = false">取消</el-button>
- <el-button type="primary" @click="submitAdd" :loading="addSaving">${ editingId ? '保存修改' : '确认新增' }</el-button>
- </template>
- </el-dialog>
- </div>
- {% endblock %}
-
- {% block js %}
- <script>
- const { createApp, ref, reactive, onMounted, computed } = Vue;
- const { Plus, Download } = ElementPlusIconsVue;
-
- const perms = {
- canAdd: {{ canAdd | dump | safe }},
- canEdit: {{ canEdit | dump | safe }},
- canExport: {{ canExport | dump | safe }},
- canAudit: {{ canAudit | dump | safe }},
- canView: {{ canView | dump | safe }}
- };
-
- const app = createApp({
- delimiters: ['${', '}'],
- setup() {
- const keyword = ref('');
- const dateRange = ref(null);
- const tagFilter = ref('');
- const activeTab = ref('all');
- 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 uploadHeaders = {};
-
- // 新增弹窗
- const addVisible = ref(false);
- const addSaving = ref(false);
- const editingId = ref(null);
- const addForm = reactive({
- name: '', phone: '', id_card: '', gender: '', birth_date: '',
- regionCodes: [], 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: ''
- });
-
- // 省市区树形数据
- const regionTree = ref([]);
-
- // 瘤种选项(从接口加载)
- 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);
-
- const tabStatusMap = { all: '', draft: '-1', pending: '0', rejected: '2', approved: '1' };
-
- async function loadTagOptions() {
- var res = await fetch('/common/tagOptions').then(function(r) { return r.json(); });
- if (res.code === 0) tagOptions.value = res.data || [];
- }
-
- async function loadRegionTree() {
- var res = await fetch('/common/regions').then(function(r) { return r.json(); });
- if (res.code === 0) regionTree.value = res.data || [];
- }
-
- async function loadList(page) {
- if (typeof page === 'number') pagination.page = page;
- loading.value = true;
- try {
- var params = new URLSearchParams({
- page: pagination.page,
- pageSize: pagination.pageSize,
- keyword: keyword.value,
- tag: tagFilter.value,
- status: tabStatusMap[activeTab.value] || ''
- });
- if (dateRange.value && dateRange.value.length === 2) {
- params.set('startDate', dateRange.value[0]);
- params.set('endDate', dateRange.value[1]);
- }
- if (regionFilter.value && regionFilter.value.length >= 1) params.set('province_code', regionFilter.value[0]);
- if (regionFilter.value && regionFilter.value.length >= 2) params.set('city_code', regionFilter.value[1]);
- if (regionFilter.value && regionFilter.value.length >= 3) params.set('district_code', regionFilter.value[2]);
- var res = await fetch('/admin/patient/list?' + params).then(function(r) { return r.json(); });
- if (res.code === 0) {
- tableData.value = res.data.data || [];
- pagination.total = res.data.count || 0;
- if (res.data.counts) Object.assign(counts, res.data.counts);
- } else {
- ElementPlus.ElMessage.error(res.msg || '加载失败');
- }
- } finally {
- loading.value = false;
- }
- }
-
- function resetFilter() {
- keyword.value = '';
- dateRange.value = null;
- tagFilter.value = '';
- regionFilter.value = [];
- activeTab.value = 'all';
- pagination.page = 1;
- loadList();
- }
-
- function onTabChange() {
- pagination.page = 1;
- loadList();
- }
-
- function onSizeChange() {
- pagination.page = 1;
- loadList();
- }
-
- function viewDetail(row) {
- if (!perms.canView) {
- ElementPlus.ElMessageBox.alert('暂无查看详情权限,请先联系管理员授权', '温馨提示', {
- confirmButtonText: '知道了',
- type: 'warning'
- });
- return;
- }
- window.location.href = '/admin/patient/detail.html?id=' + row.id;
- }
-
- function showAddDialog() {
- editingId.value = null;
- Object.assign(addForm, {
- name: '', phone: '', id_card: '', gender: '', birth_date: '',
- regionCodes: [], 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: ''
- });
- addVisible.value = true;
- }
-
- function onIdCardInput() {
- var v = addForm.id_card;
- if (v.length === 18) {
- addForm.birth_date = v.substring(6, 10) + '-' + v.substring(10, 12) + '-' + v.substring(12, 14);
- addForm.gender = parseInt(v.charAt(16)) % 2 === 0 ? '女' : '男';
- }
- }
-
- function onDocUpload(res) {
- if (res.code === 0 && res.data && res.data.url) {
- addForm.documents.push(res.data.url);
- } else {
- ElementPlus.ElMessage.error(res.msg || '上传失败');
- }
- }
-
- function onSignUpload(res, field) {
- if (res.code === 0 && res.data && res.data.url) {
- addForm[field] = res.data.url;
- } else {
- ElementPlus.ElMessage.error(res.msg || '上传失败');
- }
- }
-
- async function showEditDialog(row) {
- editingId.value = row.id;
- // 先重置表单
- Object.assign(addForm, {
- name: '', phone: '', id_card: '', gender: '', birth_date: '',
- regionCodes: [], 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: ''
- });
- try {
- var res = await fetch('/admin/patient/info?id=' + row.id).then(function(r) { return r.json(); });
- if (res.code === 0) {
- var p = res.data.patient;
- Object.assign(addForm, {
- name: p.name || '',
- phone: p.phone || '',
- id_card: p.id_card || '',
- gender: p.gender || '',
- birth_date: p.birth_date || '',
- address: p.address || '',
- emergency_contact: p.emergency_contact || '',
- emergency_phone: p.emergency_phone || '',
- tag: p.tag || '',
- documents: (typeof p.documents === 'string' ? JSON.parse(p.documents || '[]') : p.documents) || [],
- 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;
- } else {
- ElementPlus.ElMessage.error(res.msg || '获取患者信息失败');
- }
- } catch(e) {
- ElementPlus.ElMessage.error('获取患者信息失败');
- }
- }
-
- async function submitAdd() {
- if (!addForm.name.trim()) return ElementPlus.ElMessage.warning('请输入姓名');
- if (!addForm.phone || !/^1\d{10}$/.test(addForm.phone)) return ElementPlus.ElMessage.warning('请输入正确的手机号');
- if (!addForm.id_card || !/^\d{17}[\dXx]$/.test(addForm.id_card)) return ElementPlus.ElMessage.warning('请输入正确的身份证号');
- if (!addForm.birth_date) return ElementPlus.ElMessage.warning('请选择出生日期');
- if (!addForm.gender) return ElementPlus.ElMessage.warning('请选择性别');
- if (!addForm.regionCodes || addForm.regionCodes.length !== 3) return ElementPlus.ElMessage.warning('请选择省市区');
- if (!addForm.address.trim()) return ElementPlus.ElMessage.warning('请输入详细地址');
-
- addSaving.value = true;
- try {
- var url = editingId.value ? '/admin/patient/edit' : '/admin/patient/add';
- var body = {
- name: addForm.name.trim(),
- phone: addForm.phone,
- id_card: addForm.id_card,
- gender: addForm.gender,
- birth_date: addForm.birth_date,
- province_code: addForm.regionCodes[0],
- city_code: addForm.regionCodes[1],
- district_code: addForm.regionCodes[2],
- address: addForm.address.trim(),
- emergency_contact: addForm.emergency_contact || '',
- emergency_phone: addForm.emergency_phone || '',
- tag: addForm.tag || '',
- documents: addForm.documents,
- sign_income: addForm.sign_income,
- sign_privacy: addForm.sign_privacy,
- 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;
-
- var res = await fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
- }).then(function(r) { return r.json(); });
-
- if (res.code === 0) {
- ElementPlus.ElMessage.success(editingId.value ? '保存成功' : '新增成功');
- addVisible.value = false;
- loadList();
- } else {
- ElementPlus.ElMessage.error(res.msg || '操作失败');
- }
- } finally {
- addSaving.value = false;
- }
- }
-
- function handleExport() {
- var params = new URLSearchParams({
- keyword: keyword.value,
- tag: tagFilter.value,
- status: tabStatusMap[activeTab.value] || ''
- });
- if (dateRange.value && dateRange.value.length === 2) {
- params.set('startDate', dateRange.value[0]);
- params.set('endDate', dateRange.value[1]);
- }
- if (regionFilter.value && regionFilter.value.length >= 1) params.set('province_code', regionFilter.value[0]);
- if (regionFilter.value && regionFilter.value.length >= 2) params.set('city_code', regionFilter.value[1]);
- if (regionFilter.value && regionFilter.value.length >= 3) params.set('district_code', regionFilter.value[2]);
- window.open('/admin/patient/export?' + params.toString(), '_blank');
- }
-
- onMounted(function() {
- loadList();
- loadRegionTree();
- loadTagOptions();
- });
-
- return {
- keyword, dateRange, tagFilter, regionFilter, activeTab, loading, tableData, pagination, counts,
- uploadHeaders, addVisible, addSaving, addForm, exporting, editingId, perms,
- regionTree, tagOptions, isMinorComputed, Plus, Download,
- loadList, resetFilter, onTabChange, onSizeChange, viewDetail, showAddDialog, showEditDialog, handleExport,
- onIdCardInput, onDocUpload, onSignUpload, submitAdd
- };
- }
- });
-
- app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
- app.mount('#patientApp');
- </script>
- {% endblock %}
|