1831 lines
56 KiB
Vue
1831 lines
56 KiB
Vue
<template>
|
||
<div class="apt-h5">
|
||
<template v-if="currentView === 'list'">
|
||
<header class="apt-h5-topbar">
|
||
<div class="apt-h5-search">
|
||
<el-input
|
||
v-model="formData.patient_name"
|
||
placeholder="患者姓名"
|
||
clearable
|
||
:prefix-icon="Search"
|
||
@keyup.enter="handleSearch"
|
||
@clear="handleSearch"
|
||
/>
|
||
<el-button class="apt-h5-search-btn" type="primary" @click="handleSearch">查询</el-button>
|
||
</div>
|
||
|
||
<div class="apt-h5-status apt-h5-scroll">
|
||
<span
|
||
v-for="tab in statusTabs"
|
||
:key="tab.value"
|
||
:class="['apt-h5-chip', { active: activeTab === tab.value }]"
|
||
@click="handleTabChange(tab.value)"
|
||
>
|
||
{{ tab.label }}
|
||
<em v-if="tab.count">{{ tab.count }}</em>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="apt-h5-date apt-h5-scroll">
|
||
<span
|
||
v-for="item in dateTabs"
|
||
:key="item.value || 'all'"
|
||
:class="['apt-h5-pill', { active: formData.date_preset === item.value }]"
|
||
@click="handleDatePresetChange(item.value)"
|
||
>
|
||
{{ item.label }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="apt-h5-quick">
|
||
<div class="apt-h5-quick-group">
|
||
<span class="apt-h5-quick-label">确认</span>
|
||
<span
|
||
v-for="item in confirmedOptions"
|
||
:key="item.value || 'all'"
|
||
:class="['apt-h5-pill', { active: formData.diagnosis_confirmed === item.value }]"
|
||
@click="handleConfirmedChange(item.value)"
|
||
>
|
||
{{ item.label }}
|
||
</span>
|
||
</div>
|
||
<el-button link type="primary" size="small" class="apt-h5-more-toggle" @click="showMoreFilter = !showMoreFilter">
|
||
{{ showMoreFilter ? '收起' : '更多' }}
|
||
<el-icon :class="{ 'rotate-180': showMoreFilter }"><ArrowDown /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
|
||
<div v-show="showMoreFilter" class="apt-h5-more-filter">
|
||
<el-input
|
||
v-if="isAdmin"
|
||
v-model="formData.doctor_name"
|
||
placeholder="医生"
|
||
clearable
|
||
size="small"
|
||
@keyup.enter="handleSearch"
|
||
@clear="handleSearch"
|
||
/>
|
||
<el-date-picker
|
||
v-model="customDateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="YYYY-MM-DD"
|
||
size="small"
|
||
class="apt-h5-date-range"
|
||
@change="applyCustomDate"
|
||
/>
|
||
<el-button size="small" @click="handleReset">重置</el-button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="apt-h5-list" v-loading="pager.loading">
|
||
<div v-if="pager.lists.length === 0 && !pager.loading" class="apt-h5-empty">
|
||
<el-empty description="暂无问诊" :image-size="120" />
|
||
</div>
|
||
|
||
<article v-for="row in pager.lists" :key="row.id" :class="['apt-h5-card', cardClass(row)]">
|
||
<header class="apt-h5-card-head" @click="handleDetail(row)">
|
||
<div class="apt-h5-avatar">{{ (row.patient_name || '患').charAt(0) }}</div>
|
||
<div class="apt-h5-title">
|
||
<div class="apt-h5-name">
|
||
{{ row.patient_name || '-' }}
|
||
<span v-if="row.gender === 1 || row.gender === 0" class="apt-h5-meta">{{ row.gender === 1 ? '男' : '女' }}</span>
|
||
<span v-if="row.age != null" class="apt-h5-meta">{{ row.age }}岁</span>
|
||
</div>
|
||
<div class="apt-h5-sub">#{{ row.id }} · {{ maskPhone(row.patient_phone) || '-' }}</div>
|
||
</div>
|
||
<div class="apt-h5-tags">
|
||
<span :class="['apt-h5-tag', statusTagClass(row.status)]">{{ row.status_desc || statusLabel(row.status) }}</span>
|
||
<span v-if="row.diagnosis_confirmed" class="apt-h5-tag apt-h5-tag--ok">已确认</span>
|
||
<span v-else class="apt-h5-tag apt-h5-tag--warn">未确认</span>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="apt-h5-card-body" @click="handleDetail(row)">
|
||
<div class="apt-h5-row">
|
||
<span class="apt-h5-row-key">预约</span>
|
||
<span class="apt-h5-row-val">{{ row.appointment_date || '-' }} {{ row.appointment_time || '' }}</span>
|
||
</div>
|
||
<div class="apt-h5-row">
|
||
<span class="apt-h5-row-key">医生</span>
|
||
<span class="apt-h5-row-val">{{ row.doctor_name || '-' }}</span>
|
||
</div>
|
||
<div class="apt-h5-row">
|
||
<span class="apt-h5-row-key">助理</span>
|
||
<span class="apt-h5-row-val">{{ row.assistant_name || '-' }}</span>
|
||
</div>
|
||
<div class="apt-h5-row">
|
||
<span class="apt-h5-row-key">开方</span>
|
||
<span class="apt-h5-row-val">
|
||
<span v-if="row.has_prescription" class="apt-h5-inline-tag apt-h5-inline-tag--ok">已开方</span>
|
||
<span v-else class="apt-h5-inline-tag">未开方</span>
|
||
<span v-if="row.has_prescription && row.prescription_is_system_auto === 1" class="apt-h5-inline-tag apt-h5-inline-tag--warn">系统代开</span>
|
||
</span>
|
||
</div>
|
||
<div v-if="row.remark" class="apt-h5-row apt-h5-row--remark">
|
||
<span class="apt-h5-row-key">备注</span>
|
||
<span class="apt-h5-row-val">{{ row.remark }}</span>
|
||
</div>
|
||
</section>
|
||
|
||
<footer class="apt-h5-card-foot">
|
||
<el-button
|
||
v-if="hasPermission(['tcm.diagnosis/edit'])"
|
||
type="primary"
|
||
size="small"
|
||
plain
|
||
@click="handleEdit(row)"
|
||
>
|
||
编辑患者
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.status !== 3 && hasPermission(['doctor.appointment/prescription'])"
|
||
type="primary"
|
||
size="small"
|
||
:icon="Phone"
|
||
@click="handleChat(row)"
|
||
>
|
||
通话
|
||
</el-button>
|
||
<el-button
|
||
v-if="hasPermission(['tcm.diagnosis/kaifang'])"
|
||
:type="prescriptionActionLabel(row) === '查看' ? 'success' : 'primary'"
|
||
size="small"
|
||
plain
|
||
@click="handlePrescription(row)"
|
||
>
|
||
{{ prescriptionActionLabel(row) }}
|
||
</el-button>
|
||
<el-button size="small" class="apt-h5-more-btn" @click="openMoreSheet(row)">
|
||
更多<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||
</el-button>
|
||
</footer>
|
||
</article>
|
||
|
||
<div v-if="hasMore" class="apt-h5-load-more" @click="loadMore">
|
||
{{ pager.loading ? '加载中...' : '加载更多' }}
|
||
</div>
|
||
<div v-if="!hasMore && pager.lists.length > 0" class="apt-h5-no-more">没有更多了</div>
|
||
</main>
|
||
|
||
<el-drawer
|
||
v-model="moreSheetVisible"
|
||
:title="moreSheetRow ? `${moreSheetRow.patient_name || '问诊'} · 更多操作` : '更多操作'"
|
||
direction="btt"
|
||
size="auto"
|
||
:destroy-on-close="true"
|
||
>
|
||
<div v-if="moreSheetRow" class="apt-h5-actions">
|
||
<button
|
||
v-if="moreSheetRow.status !== 3 && hasPermission(['tcm.diagnosis/videoQr'])"
|
||
class="apt-h5-action"
|
||
@click="runAction('videoQr')"
|
||
>
|
||
<el-icon><Picture /></el-icon>
|
||
<span>视频二维码</span>
|
||
</button>
|
||
<button
|
||
v-if="moreSheetRow.status !== 3 && hasPermission(['doctor.appointment/complete'])"
|
||
class="apt-h5-action"
|
||
@click="runAction('complete')"
|
||
>
|
||
<el-icon><CircleCheck /></el-icon>
|
||
<span>完成</span>
|
||
</button>
|
||
<button
|
||
v-if="hasPermission(['tcm.diagnosis/kaifang'])"
|
||
class="apt-h5-action"
|
||
@click="runAction('case')"
|
||
>
|
||
<el-icon><Document /></el-icon>
|
||
<span>病历</span>
|
||
</button>
|
||
<button
|
||
v-if="moreSheetRow.status === 1 && hasPermission(['doctor.appointment/cancel'])"
|
||
class="apt-h5-action apt-h5-action--danger"
|
||
@click="runAction('cancel')"
|
||
>
|
||
<el-icon><CircleClose /></el-icon>
|
||
<span>取消挂号</span>
|
||
</button>
|
||
</div>
|
||
</el-drawer>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<header class="apt-h5-detail-topbar">
|
||
<button class="apt-h5-back" @click="backToList">
|
||
<span>‹</span>
|
||
返回
|
||
</button>
|
||
<div class="apt-h5-detail-title">
|
||
<strong>{{ detailData?.patient_name || detailSourceRow?.patient_name || '问诊详情' }}</strong>
|
||
<small>{{ detailData?.appointment_date || detailSourceRow?.appointment_date || '' }} {{ detailData?.appointment_time || detailSourceRow?.appointment_time || '' }}</small>
|
||
</div>
|
||
</header>
|
||
|
||
<main v-loading="detailLoading" class="apt-h5-detail-page">
|
||
<template v-if="detailData">
|
||
<section class="apt-h5-detail-section">
|
||
<h3>预约信息</h3>
|
||
<div class="apt-h5-detail-grid">
|
||
<div><span>挂号ID</span><strong>{{ detailData.id || '-' }}</strong></div>
|
||
<div><span>状态</span><strong>{{ detailData.status_desc || statusLabel(detailData.status) }}</strong></div>
|
||
<div><span>医生</span><strong>{{ detailData.doctor_name || '-' }}</strong></div>
|
||
<div><span>预约时间</span><strong>{{ detailData.appointment_date || '-' }} {{ detailData.appointment_time || '' }}</strong></div>
|
||
<div><span>时段</span><strong>{{ detailData.period === 'morning' ? '上午' : detailData.period === 'afternoon' ? '下午' : '-' }}</strong></div>
|
||
<div><span>预约类型</span><strong>{{ detailData.appointment_type_desc || '-' }}</strong></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="apt-h5-detail-section">
|
||
<h3>患者信息</h3>
|
||
<div class="apt-h5-detail-grid">
|
||
<div><span>姓名</span><strong>{{ detailData.patient_name || '-' }}</strong></div>
|
||
<div><span>手机号</span><strong>{{ detailData.phone || detailData.patient_phone || '-' }}</strong></div>
|
||
<div class="apt-h5-detail-full"><span>身份证号</span><strong>{{ detailData.id_card || '-' }}</strong></div>
|
||
<div><span>性别</span><strong>{{ detailData.gender === 1 ? '男' : detailData.gender === 0 ? '女' : '-' }}</strong></div>
|
||
<div><span>年龄</span><strong>{{ detailData.age != null ? `${detailData.age}岁` : '-' }}</strong></div>
|
||
<div><span>诊断日期</span><strong>{{ detailData.diagnosis_date || '-' }}</strong></div>
|
||
<div><span>婚姻状态</span><strong>{{ maritalStatusLabel(detailData.marital_status) }}</strong></div>
|
||
<div><span>身高(cm)</span><strong>{{ detailData.height ?? '-' }}</strong></div>
|
||
<div><span>体重(kg)</span><strong>{{ detailData.weight ?? '-' }}</strong></div>
|
||
<div class="apt-h5-detail-full"><span>地区</span><strong>{{ detailData.region || '-' }}</strong></div>
|
||
<div><span>收缩压(mmHg)</span><strong>{{ detailData.systolic_pressure ?? '-' }}</strong></div>
|
||
<div><span>舒张压(mmHg)</span><strong>{{ detailData.diastolic_pressure ?? '-' }}</strong></div>
|
||
<div><span>空腹血糖(mmol/L)</span><strong>{{ detailData.fasting_blood_sugar ?? '-' }}</strong></div>
|
||
<div><span>诊断类型</span><strong>{{ getDictLabel(diagnosisTypeOptions, detailData.diagnosis_type) }}</strong></div>
|
||
<div><span>证型</span><strong>{{ getDictLabel(syndromeTypeOptions, detailData.syndrome_type) }}</strong></div>
|
||
<div><span>糖尿病期数</span><strong>{{ getDictLabel(diabetesTypeOptions, detailData.diabetes_type) }}</strong></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="hasHistoryDetail(detailData)" class="apt-h5-detail-section">
|
||
<h3>病史信息</h3>
|
||
<div class="apt-h5-detail-grid">
|
||
<div v-if="isDiabetesDiscoveryFilled(detailData.diabetes_discovery_year)">
|
||
<span>发现糖尿病病史</span>
|
||
<strong>{{ formatDiabetesDiscoveryDisplay(detailData.diabetes_discovery_year) }}</strong>
|
||
</div>
|
||
<div v-if="detailData.local_hospital_name">
|
||
<span>当地就诊医院</span>
|
||
<strong>{{ detailData.local_hospital_name }}</strong>
|
||
</div>
|
||
<div v-if="detailData.symptoms" class="apt-h5-detail-full">
|
||
<span>症状补充</span>
|
||
<strong>{{ detailData.symptoms }}</strong>
|
||
</div>
|
||
<div v-if="detailData.past_history && detailData.past_history.length" class="apt-h5-detail-full">
|
||
<span>既往史</span>
|
||
<strong>{{ getPastHistoryLabels(detailData.past_history) }}</strong>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="hasDiagnosisDetail(detailData)" class="apt-h5-detail-section">
|
||
<h3>诊断信息</h3>
|
||
<div class="apt-h5-detail-grid">
|
||
<div v-if="detailData.tongue_coating">
|
||
<span>舌苔</span>
|
||
<strong>{{ detailData.tongue_coating }}</strong>
|
||
</div>
|
||
<div v-if="detailData.pulse">
|
||
<span>脉象</span>
|
||
<strong>{{ detailData.pulse }}</strong>
|
||
</div>
|
||
<div v-if="detailData.treatment_principle" class="apt-h5-detail-full">
|
||
<span>治则</span>
|
||
<strong>{{ detailData.treatment_principle }}</strong>
|
||
</div>
|
||
<div v-if="detailData.prescription" class="apt-h5-detail-full">
|
||
<span>处方</span>
|
||
<strong>{{ detailData.prescription }}</strong>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="apt-h5-detail-section">
|
||
<h3>备注</h3>
|
||
<p>{{ detailData.remark || '无' }}</p>
|
||
</section>
|
||
</template>
|
||
</main>
|
||
|
||
<footer v-if="detailData" class="apt-h5-detail-footer-bar">
|
||
<div class="apt-h5-detail-footer">
|
||
<el-button
|
||
v-if="hasPermission(['tcm.diagnosis/edit'])"
|
||
type="primary"
|
||
plain
|
||
@click="handleEdit(detailActionRow)"
|
||
>
|
||
编辑患者
|
||
</el-button>
|
||
<el-button
|
||
v-if="hasPermission(['tcm.diagnosis/kaifang'])"
|
||
type="primary"
|
||
@click="handlePrescription(detailActionRow)"
|
||
>
|
||
{{ prescriptionActionLabel(detailActionRow) }}
|
||
</el-button>
|
||
<el-button
|
||
v-if="detailActionRow.status !== 3 && hasPermission(['doctor.appointment/prescription'])"
|
||
type="primary"
|
||
plain
|
||
@click="handleChat(detailActionRow)"
|
||
>
|
||
通话
|
||
</el-button>
|
||
<el-button
|
||
v-if="detailActionRow.status !== 3 && hasPermission(['doctor.appointment/complete'])"
|
||
type="success"
|
||
plain
|
||
@click="handleComplete(detailActionRow)"
|
||
>
|
||
完成
|
||
</el-button>
|
||
</div>
|
||
</footer>
|
||
</template>
|
||
|
||
<el-dialog
|
||
v-model="qrcodeDialogVisible"
|
||
title="视频二维码"
|
||
:close-on-click-modal="false"
|
||
class="apt-h5-qr-dialog"
|
||
>
|
||
<div class="apt-h5-qr">
|
||
<div v-if="qrcodeLoading" class="apt-h5-qr-loading">
|
||
<el-icon class="is-loading" :size="36"><Loading /></el-icon>
|
||
<div>生成中...</div>
|
||
</div>
|
||
<div v-else-if="qrcodeUrl">
|
||
<img :src="qrcodeUrl" alt="小程序二维码" class="apt-h5-qr-img" />
|
||
<div class="apt-h5-qr-meta">
|
||
<div>患者:{{ currentQRCodeRow?.patient_name || '-' }}</div>
|
||
<div>请使用微信扫描二维码</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="apt-h5-qr-fail">生成失败,请重试</div>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="qrcodeDialogVisible = false">关闭</el-button>
|
||
<el-button v-if="!qrcodeLoading" type="primary" @click="handleRegenerateQRCode">重新生成</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="completeDialogVisible"
|
||
title="完成问诊"
|
||
:close-on-click-modal="false"
|
||
class="apt-h5-complete-dialog"
|
||
destroy-on-close
|
||
>
|
||
<div class="apt-h5-complete">
|
||
<p>确认患者已就诊完成?</p>
|
||
<el-input
|
||
v-if="canAddCompleteNote"
|
||
v-model="completeNoteContent"
|
||
type="textarea"
|
||
:rows="4"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="填写完成备注,将追加到医生备注时间轴"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="completeDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="completeSubmitting" @click="submitComplete">确认完成</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<edit-popup ref="editRef" @success="() => loadData({ silent: true })" />
|
||
<tcm-prescription ref="prescriptionRef" @success="onPrescriptionSuccess" />
|
||
<chat-dialog ref="chatDialogRef" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="tcmAppointmentH5">
|
||
import { appointmentDetail, appointmentLists, cancelAppointment, completeAppointment } from '@/api/doctor'
|
||
import { getWeappConfig } from '@/api/channel/weapp'
|
||
import {
|
||
generateMiniProgramQrcode,
|
||
getCallSignature,
|
||
prescriptionGetByAppointment,
|
||
tcmDiagnosisDetail
|
||
} from '@/api/tcm'
|
||
import { getDictData } from '@/api/app'
|
||
import { addDoctorNote } from '@/api/patient'
|
||
import { usePaging } from '@/hooks/usePaging'
|
||
import useUserStore from '@/stores/modules/user'
|
||
import {
|
||
formatDiabetesDiscoveryDisplay,
|
||
isDiabetesDiscoveryFilled
|
||
} from '@/utils/diabetes-discovery-display'
|
||
import feedback from '@/utils/feedback'
|
||
import { hasPermission } from '@/utils/perm'
|
||
import {
|
||
ArrowDown,
|
||
CircleCheck,
|
||
CircleClose,
|
||
Document,
|
||
Loading,
|
||
Phone,
|
||
Picture,
|
||
Search
|
||
} from '@element-plus/icons-vue'
|
||
import { computed, defineAsyncComponent, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||
|
||
const EditPopup = defineAsyncComponent(() => import('../diagnosis/edit.vue'))
|
||
const TcmPrescription = defineAsyncComponent(() => import('@/components/tcm-prescription/index.vue'))
|
||
const ChatDialog = defineAsyncComponent(() => import('@/components/chat-dialog/index.vue'))
|
||
|
||
const userStore = useUserStore()
|
||
const userInfo = computed(() => userStore.userInfo)
|
||
|
||
const todayDate = () => {
|
||
const t = new Date()
|
||
return formatDate(t)
|
||
}
|
||
const formatDate = (date: Date) => {
|
||
const pad = (n: number) => String(n).padStart(2, '0')
|
||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||
}
|
||
const dateByOffset = (offset: number) => {
|
||
const t = new Date()
|
||
t.setDate(t.getDate() + offset)
|
||
return formatDate(t)
|
||
}
|
||
const maskPhone = (phone: string) => {
|
||
if (!phone || phone.length < 11) return phone
|
||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||
}
|
||
|
||
const isAdmin = computed(() => {
|
||
const roleId = userInfo.value?.role_id
|
||
if (Array.isArray(roleId)) return !roleId.includes(1) && !roleId.includes(2)
|
||
return roleId !== 1 && roleId !== 2
|
||
})
|
||
|
||
const formData = reactive({
|
||
patient_name: '',
|
||
doctor_name: '',
|
||
status: 1 as string | number,
|
||
start_date: todayDate(),
|
||
end_date: todayDate(),
|
||
date_preset: 'today' as '' | 'yesterday' | 'day_before' | 'today' | 'tomorrow' | 'day_after',
|
||
diagnosis_confirmed: '' as '' | '0' | '1',
|
||
include_status_counts: 0 as 0 | 1
|
||
})
|
||
const activeTab = ref('1')
|
||
const currentView = ref<'list' | 'detail'>('list')
|
||
const showMoreFilter = ref(false)
|
||
const customDateRange = ref<[string, string] | ''>('')
|
||
const statusCount = ref<Record<number, number>>({ 1: 0, 2: 0, 3: 0, 4: 0 })
|
||
|
||
const { pager, getLists } = usePaging({
|
||
fetchFun: appointmentLists,
|
||
params: formData,
|
||
firstLoading: true
|
||
})
|
||
|
||
const statusTabs = computed(() => [
|
||
{ label: '待接诊', value: '1', count: statusCount.value[1] },
|
||
{ label: '全部', value: 'all', count: 0 },
|
||
{ label: '已过号', value: '4', count: statusCount.value[4] },
|
||
{ label: '已取消', value: '2', count: statusCount.value[2] },
|
||
{ label: '已完成', value: '3', count: statusCount.value[3] }
|
||
])
|
||
const dateTabs = [
|
||
{ label: '昨天', value: 'yesterday' },
|
||
{ label: '前天', value: 'day_before' },
|
||
{ label: '今天', value: 'today' },
|
||
{ label: '明天', value: 'tomorrow' },
|
||
{ label: '后天', value: 'day_after' },
|
||
{ label: '不限', value: '' }
|
||
] as const
|
||
const confirmedOptions = [
|
||
{ label: '全部', value: '' },
|
||
{ label: '已确认', value: '1' },
|
||
{ label: '未确认', value: '0' }
|
||
] as const
|
||
const hasMore = computed(() => pager.lists.length < (pager.count || 0))
|
||
|
||
const loadData = async (opts?: { silent?: boolean }) => {
|
||
formData.include_status_counts = 1
|
||
try {
|
||
await getLists(opts)
|
||
const sc = (pager.extend as { status_count?: Record<string | number, number> })?.status_count
|
||
if (sc) {
|
||
;([1, 2, 3, 4] as const).forEach((key) => {
|
||
const n = sc[key] ?? sc[String(key)]
|
||
statusCount.value[key] = typeof n === 'number' ? n : 0
|
||
})
|
||
}
|
||
} finally {
|
||
formData.include_status_counts = 0
|
||
}
|
||
}
|
||
|
||
const handleSearch = () => {
|
||
pager.page = 1
|
||
loadData()
|
||
}
|
||
const handleTabChange = (tab: string) => {
|
||
activeTab.value = tab
|
||
formData.status = tab === 'all' ? '' : Number(tab)
|
||
pager.page = 1
|
||
loadData()
|
||
}
|
||
const handleDatePresetChange = (value: string) => {
|
||
formData.date_preset = value as typeof formData.date_preset
|
||
customDateRange.value = ''
|
||
if (!value) {
|
||
formData.start_date = ''
|
||
formData.end_date = ''
|
||
} else {
|
||
const offsetMap: Record<string, number> = {
|
||
yesterday: -1,
|
||
day_before: -2,
|
||
today: 0,
|
||
tomorrow: 1,
|
||
day_after: 2
|
||
}
|
||
const date = dateByOffset(offsetMap[value] ?? 0)
|
||
formData.start_date = date
|
||
formData.end_date = date
|
||
}
|
||
pager.page = 1
|
||
loadData()
|
||
}
|
||
const handleConfirmedChange = (value: '' | '0' | '1') => {
|
||
formData.diagnosis_confirmed = value
|
||
pager.page = 1
|
||
loadData()
|
||
}
|
||
const applyCustomDate = () => {
|
||
if (!customDateRange.value) {
|
||
handleDatePresetChange('')
|
||
return
|
||
}
|
||
formData.date_preset = ''
|
||
formData.start_date = customDateRange.value[0]
|
||
formData.end_date = customDateRange.value[1]
|
||
pager.page = 1
|
||
loadData()
|
||
}
|
||
const handleReset = () => {
|
||
formData.patient_name = ''
|
||
formData.doctor_name = ''
|
||
formData.diagnosis_confirmed = ''
|
||
activeTab.value = '1'
|
||
formData.status = 1
|
||
handleDatePresetChange('today')
|
||
}
|
||
const loadMore = async () => {
|
||
if (pager.loading || !hasMore.value) return
|
||
const next = (pager.page || 1) + 1
|
||
pager.loading = true
|
||
try {
|
||
const res = await appointmentLists({
|
||
page_no: next,
|
||
page_size: pager.size,
|
||
...formData,
|
||
include_status_counts: 0
|
||
})
|
||
pager.page = next
|
||
pager.lists.push(...(res?.lists || []))
|
||
pager.count = res?.count ?? pager.count
|
||
} finally {
|
||
pager.loading = false
|
||
}
|
||
}
|
||
|
||
const statusLabel = (status: number | string) => {
|
||
const s = Number(status)
|
||
if (s === 1) return '待接诊'
|
||
if (s === 2) return '已取消'
|
||
if (s === 3) return '已完成'
|
||
if (s === 4) return '已过号'
|
||
return '-'
|
||
}
|
||
const statusTagClass = (status: number | string) => {
|
||
const s = Number(status)
|
||
return {
|
||
'apt-h5-tag--ok': s === 1 || s === 3,
|
||
'apt-h5-tag--gray': s === 2,
|
||
'apt-h5-tag--danger': s === 4
|
||
}
|
||
}
|
||
const cardClass = (row: any) => ({
|
||
'apt-h5-card--done': Number(row.status) === 3,
|
||
'apt-h5-card--cancel': Number(row.status) === 2,
|
||
'apt-h5-card--missed': Number(row.status) === 4
|
||
})
|
||
|
||
const editRef = ref()
|
||
const prescriptionRef = ref()
|
||
const chatDialogRef = ref()
|
||
const COMPLETE_NOTE_PERMISSION = ['doctor.appointment/addDoctorNote']
|
||
const completeDialogVisible = ref(false)
|
||
const completeSubmitting = ref(false)
|
||
const completeNoteContent = ref('')
|
||
const completeRow = ref<any>(null)
|
||
const canAddCompleteNote = computed(() => hasPermission(COMPLETE_NOTE_PERMISSION))
|
||
|
||
const handleEdit = (row: any) => {
|
||
if (!row.patient_id) {
|
||
feedback.msgWarning('该预约没有关联诊单信息')
|
||
return
|
||
}
|
||
editRef.value?.open('edit', row.patient_id)
|
||
}
|
||
const handleChat = async (row: any) => {
|
||
if (!row.patient_id) {
|
||
feedback.msgWarning('患者信息不完整')
|
||
return
|
||
}
|
||
try {
|
||
const res = await getCallSignature({
|
||
patient_id: row.patient_id,
|
||
diagnosis_id: row.diagnosis_id || row.id
|
||
})
|
||
chatDialogRef.value?.open({
|
||
patientId: row.patient_id,
|
||
patientName: row.patient_name,
|
||
diagnosisId: row.diagnosis_id || row.id,
|
||
signatureData: res
|
||
})
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || e?.message || '获取通话签名失败')
|
||
}
|
||
}
|
||
const prescriptionActionLabel = (row: any) => {
|
||
if (row?.prescription_audit_status === 1 && Number(row?.prescription_void_status) !== 1) return '查看'
|
||
return '开方'
|
||
}
|
||
const handlePrescription = (row: any) => {
|
||
if (!row.diagnosis_id && !row.patient_id) {
|
||
feedback.msgWarning('该预约缺少诊单信息,请先编辑患者')
|
||
return
|
||
}
|
||
prescriptionRef.value?.open(row)
|
||
}
|
||
const onPrescriptionSuccess = () => {
|
||
loadData()
|
||
editRef.value?.refreshCaseList?.()
|
||
}
|
||
|
||
const moreSheetVisible = ref(false)
|
||
const moreSheetRow = ref<any>(null)
|
||
const openMoreSheet = (row: any) => {
|
||
moreSheetRow.value = row
|
||
moreSheetVisible.value = true
|
||
}
|
||
const runAction = (cmd: string) => {
|
||
const row = moreSheetRow.value
|
||
moreSheetVisible.value = false
|
||
if (!row) return
|
||
if (cmd === 'videoQr') handleMiniProgramQRCode(row)
|
||
if (cmd === 'complete') handleComplete(row)
|
||
if (cmd === 'case') handleViewCase(row)
|
||
if (cmd === 'cancel') handleCancel(row)
|
||
}
|
||
const handleComplete = async (row: any) => {
|
||
completeRow.value = row
|
||
completeNoteContent.value = ''
|
||
if (canAddCompleteNote.value) {
|
||
completeDialogVisible.value = true
|
||
return
|
||
}
|
||
try {
|
||
await feedback.confirm('确认患者已就诊完成?')
|
||
await completeAppointment({ id: row.id })
|
||
feedback.msgSuccess('操作成功')
|
||
loadData()
|
||
} catch (e: any) {
|
||
if (e !== 'cancel') feedback.msgError(e?.msg || '操作失败')
|
||
}
|
||
}
|
||
const submitComplete = async () => {
|
||
const row = completeRow.value
|
||
if (!row?.id || completeSubmitting.value) return
|
||
completeSubmitting.value = true
|
||
try {
|
||
const note = completeNoteContent.value.trim()
|
||
await completeAppointment({ id: row.id })
|
||
let noteFailureMsg = ''
|
||
if (canAddCompleteNote.value && note) {
|
||
const diagnosisId = row.patient_id || row.diagnosis_id
|
||
if (diagnosisId) {
|
||
try {
|
||
await addDoctorNote({ diagnosis_id: Number(diagnosisId), content: note })
|
||
} catch (e) {
|
||
noteFailureMsg = '问诊已完成,但备注未保存'
|
||
}
|
||
} else {
|
||
noteFailureMsg = '问诊已完成,但该预约没有关联诊单,备注未保存'
|
||
}
|
||
}
|
||
noteFailureMsg ? feedback.msgWarning(noteFailureMsg) : feedback.msgSuccess('操作成功')
|
||
completeDialogVisible.value = false
|
||
completeNoteContent.value = ''
|
||
completeRow.value = null
|
||
if (currentView.value === 'detail') {
|
||
backToList()
|
||
}
|
||
loadData()
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || e?.message || '操作失败')
|
||
} finally {
|
||
completeSubmitting.value = false
|
||
}
|
||
}
|
||
const handleCancel = async (row: any) => {
|
||
try {
|
||
await feedback.confirm('确定要取消该挂号吗?')
|
||
await cancelAppointment({ id: row.id })
|
||
feedback.msgSuccess('取消成功')
|
||
loadData()
|
||
} catch (e: any) {
|
||
if (e !== 'cancel') feedback.msgError(e?.msg || '取消失败')
|
||
}
|
||
}
|
||
const handleViewCase = async (row: any) => {
|
||
try {
|
||
const prescription = await prescriptionGetByAppointment({ appointment_id: row.id })
|
||
if (prescription?.id) {
|
||
prescriptionRef.value?.openById(prescription.id)
|
||
} else {
|
||
feedback.msgWarning('该预约暂无病历记录,请先开方')
|
||
}
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || '获取病历失败')
|
||
}
|
||
}
|
||
|
||
const qrcodeDialogVisible = ref(false)
|
||
const qrcodeLoading = ref(false)
|
||
const qrcodeUrl = ref('')
|
||
const currentQRCodeRow = ref<any>(null)
|
||
const handleMiniProgramQRCode = async (row: any) => {
|
||
if (!row.patient_id) {
|
||
feedback.msgWarning('患者信息不完整')
|
||
return
|
||
}
|
||
currentQRCodeRow.value = row
|
||
qrcodeDialogVisible.value = true
|
||
qrcodeLoading.value = true
|
||
qrcodeUrl.value = ''
|
||
try {
|
||
const config = await getWeappConfig()
|
||
if (!config?.app_id) {
|
||
feedback.msgError('小程序未配置,请先配置小程序信息')
|
||
qrcodeDialogVisible.value = false
|
||
return
|
||
}
|
||
const result = await generateMiniProgramQrcode({
|
||
diagnosis_id: row.doctor_id || '',
|
||
patient_id: row.patient_id,
|
||
share_user_id: userStore.userInfo?.id || '',
|
||
mini_program_path: 'pages/login/login'
|
||
})
|
||
if (result?.qrcode_url) qrcodeUrl.value = result.qrcode_url
|
||
else feedback.msgError('二维码生成失败')
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || '生成二维码失败,请重试')
|
||
} finally {
|
||
qrcodeLoading.value = false
|
||
}
|
||
}
|
||
const handleRegenerateQRCode = () => {
|
||
if (currentQRCodeRow.value) handleMiniProgramQRCode(currentQRCodeRow.value)
|
||
}
|
||
|
||
const detailLoading = ref(false)
|
||
const detailData = ref<any>(null)
|
||
const detailSourceRow = ref<any>(null)
|
||
const detailActionRow = computed(() => ({ ...(detailSourceRow.value || {}), ...(detailData.value || {}) }))
|
||
const diagnosisTypeOptions = ref<any[]>([])
|
||
const syndromeTypeOptions = ref<any[]>([])
|
||
const diabetesTypeOptions = ref<any[]>([])
|
||
const pastHistoryOptions = ref<any[]>([])
|
||
const detailDictReady = ref(false)
|
||
|
||
const getDictLabel = (options: any[], value: string) => {
|
||
if (!value) return '-'
|
||
const item = options.find((opt: any) => opt.value === value)
|
||
return item ? item.name : value
|
||
}
|
||
const getPastHistoryLabels = (values: string[] | string) => {
|
||
const arr = Array.isArray(values) ? values : typeof values === 'string' ? values.split(/[,,、]/) : []
|
||
return arr.map((v: string) => getDictLabel(pastHistoryOptions.value, v.trim())).filter(Boolean).join('、') || '-'
|
||
}
|
||
const maritalStatusLabel = (value: number | string | undefined) => {
|
||
if (Number(value) === 0) return '未婚'
|
||
if (Number(value) === 1) return '已婚'
|
||
if (Number(value) === 2) return '离异'
|
||
return '-'
|
||
}
|
||
const loadDetailDictOptions = async () => {
|
||
const [dt, st, db, ph] = await Promise.all([
|
||
getDictData({ type: 'diagnosis_type' }),
|
||
getDictData({ type: 'syndrome_type' }),
|
||
getDictData({ type: 'diabetes_type' }),
|
||
getDictData({ type: 'past_history' })
|
||
])
|
||
diagnosisTypeOptions.value = dt?.diagnosis_type || []
|
||
syndromeTypeOptions.value = st?.syndrome_type || []
|
||
diabetesTypeOptions.value = db?.diabetes_type || []
|
||
pastHistoryOptions.value = ph?.past_history || []
|
||
}
|
||
const ensureDetailDictOptions = async () => {
|
||
if (detailDictReady.value) return
|
||
await loadDetailDictOptions()
|
||
detailDictReady.value = true
|
||
}
|
||
const hasHistoryDetail = (data: any) =>
|
||
isDiabetesDiscoveryFilled(data?.diabetes_discovery_year) ||
|
||
!!data?.local_hospital_name ||
|
||
!!data?.symptoms ||
|
||
(Array.isArray(data?.past_history) ? data.past_history.length > 0 : !!data?.past_history)
|
||
const hasDiagnosisDetail = (data: any) =>
|
||
!!(data?.tongue_coating || data?.pulse || data?.treatment_principle || data?.prescription)
|
||
|
||
const handleDetail = async (row: any) => {
|
||
currentView.value = 'detail'
|
||
detailLoading.value = true
|
||
detailData.value = null
|
||
detailSourceRow.value = row
|
||
try {
|
||
const [, appointment] = await Promise.all([
|
||
ensureDetailDictOptions().catch((e) => console.warn('加载详情字典失败:', e)),
|
||
appointmentDetail({ id: row.id })
|
||
])
|
||
detailData.value = { ...row, ...appointment, appointment_id: row.id }
|
||
if (appointment?.patient_id) {
|
||
try {
|
||
const diagnosis = await tcmDiagnosisDetail({ id: appointment.patient_id })
|
||
detailData.value = {
|
||
...row,
|
||
...appointment,
|
||
...diagnosis,
|
||
id: appointment.id || row.id,
|
||
appointment_id: row.id,
|
||
diagnosis_id: appointment.patient_id
|
||
}
|
||
} catch (e) {
|
||
console.warn('获取患者诊单详情失败:', e)
|
||
}
|
||
}
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || '获取详情失败')
|
||
currentView.value = 'list'
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
const backToList = () => {
|
||
currentView.value = 'list'
|
||
detailLoading.value = false
|
||
}
|
||
|
||
const LIST_POLL_MS = 20_000
|
||
let listPollTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
onMounted(() => {
|
||
document.body.classList.add('h5-mode')
|
||
loadData()
|
||
listPollTimer = setInterval(() => {
|
||
loadData({ silent: true }).catch(() => undefined)
|
||
}, LIST_POLL_MS)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
document.body.classList.remove('h5-mode')
|
||
if (listPollTimer !== null) {
|
||
clearInterval(listPollTimer)
|
||
listPollTimer = null
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.apt-h5 {
|
||
max-width: 560px;
|
||
margin: 0 auto;
|
||
min-height: 100vh;
|
||
background: #f5f7fb;
|
||
color: #1f2937;
|
||
padding-bottom: 24px;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
.apt-h5-topbar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
padding: 12px 12px 10px;
|
||
background: rgba(245, 247, 251, 0.96);
|
||
border-bottom: 1px solid #e5e7eb;
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
.apt-h5-search {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.apt-h5-search :deep(.el-input__wrapper) {
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.apt-h5-search-btn {
|
||
flex: 0 0 72px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.apt-h5-scroll {
|
||
display: flex;
|
||
gap: 8px;
|
||
overflow-x: auto;
|
||
padding: 10px 0 0;
|
||
scrollbar-width: none;
|
||
}
|
||
|
||
.apt-h5-scroll::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.apt-h5-chip,
|
||
.apt-h5-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 0 0 auto;
|
||
min-height: 30px;
|
||
padding: 0 12px;
|
||
border: 1px solid #d8dee8;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
color: #4b5563;
|
||
font-size: 13px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.apt-h5-chip em {
|
||
min-width: 18px;
|
||
height: 18px;
|
||
margin-left: 5px;
|
||
padding: 0 5px;
|
||
border-radius: 9px;
|
||
background: #eef2ff;
|
||
color: #2563eb;
|
||
font-style: normal;
|
||
font-size: 11px;
|
||
line-height: 18px;
|
||
text-align: center;
|
||
}
|
||
|
||
.apt-h5-chip.active,
|
||
.apt-h5-pill.active {
|
||
border-color: #2563eb;
|
||
background: #2563eb;
|
||
color: #fff;
|
||
}
|
||
|
||
.apt-h5-chip.active em {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
color: #fff;
|
||
}
|
||
|
||
.apt-h5-quick {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
padding-top: 10px;
|
||
}
|
||
|
||
.apt-h5-quick-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
overflow-x: auto;
|
||
scrollbar-width: none;
|
||
}
|
||
|
||
.apt-h5-quick-group::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.apt-h5-quick-label {
|
||
flex: 0 0 auto;
|
||
color: #6b7280;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.apt-h5-more-toggle {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.rotate-180 {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.apt-h5-more-filter {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
padding-top: 10px;
|
||
}
|
||
|
||
.apt-h5-date-range {
|
||
width: 100% !important;
|
||
}
|
||
|
||
.apt-h5-list {
|
||
min-height: calc(100vh - 150px);
|
||
padding: 12px;
|
||
}
|
||
|
||
.apt-h5-empty {
|
||
margin-top: 48px;
|
||
}
|
||
|
||
.apt-h5-card {
|
||
margin-bottom: 10px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.05);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.apt-h5-card--missed {
|
||
border-color: #fecaca;
|
||
}
|
||
|
||
.apt-h5-card--cancel,
|
||
.apt-h5-card--done {
|
||
opacity: 0.86;
|
||
}
|
||
|
||
.apt-h5-card-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
padding: 12px 12px 10px;
|
||
}
|
||
|
||
.apt-h5-avatar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 0 0 40px;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
background: #eff6ff;
|
||
color: #2563eb;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.apt-h5-title {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.apt-h5-name {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
color: #111827;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
line-height: 22px;
|
||
}
|
||
|
||
.apt-h5-meta {
|
||
color: #6b7280;
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.apt-h5-sub {
|
||
margin-top: 3px;
|
||
color: #8a94a6;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.apt-h5-tags {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 5px;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.apt-h5-tag,
|
||
.apt-h5-inline-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-height: 22px;
|
||
padding: 0 7px;
|
||
border-radius: 6px;
|
||
background: #eef2f7;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.apt-h5-tag--ok,
|
||
.apt-h5-inline-tag--ok {
|
||
background: #ecfdf3;
|
||
color: #15803d;
|
||
}
|
||
|
||
.apt-h5-tag--warn,
|
||
.apt-h5-inline-tag--warn {
|
||
background: #fff7ed;
|
||
color: #c2410c;
|
||
}
|
||
|
||
.apt-h5-tag--danger {
|
||
background: #fef2f2;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.apt-h5-tag--gray {
|
||
background: #f3f4f6;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.apt-h5-card-body {
|
||
padding: 0 12px 10px;
|
||
}
|
||
|
||
.apt-h5-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
padding: 5px 0;
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.apt-h5-row-key {
|
||
flex: 0 0 38px;
|
||
color: #8a94a6;
|
||
}
|
||
|
||
.apt-h5-row-val {
|
||
min-width: 0;
|
||
flex: 1;
|
||
color: #374151;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.apt-h5-row--remark .apt-h5-row-val {
|
||
color: #6b7280;
|
||
}
|
||
|
||
.apt-h5-card-foot {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 7px;
|
||
padding: 10px 12px 12px;
|
||
border-top: 1px solid #f1f5f9;
|
||
}
|
||
|
||
.apt-h5-card-foot :deep(.el-button) {
|
||
min-width: 0;
|
||
margin: 0;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.apt-h5-more-btn {
|
||
color: #4b5563;
|
||
}
|
||
|
||
.apt-h5-load-more,
|
||
.apt-h5-no-more {
|
||
padding: 14px 0 18px;
|
||
color: #6b7280;
|
||
font-size: 13px;
|
||
text-align: center;
|
||
}
|
||
|
||
.apt-h5-load-more {
|
||
color: #2563eb;
|
||
}
|
||
|
||
.apt-h5-actions {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 10px;
|
||
padding: 4px 4px 16px;
|
||
}
|
||
|
||
.apt-h5-action {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
min-height: 72px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
color: #374151;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.apt-h5-action :deep(.el-icon) {
|
||
font-size: 22px;
|
||
color: #2563eb;
|
||
}
|
||
|
||
.apt-h5-action--danger {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.apt-h5-action--danger :deep(.el-icon) {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.apt-h5-detail-topbar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 20;
|
||
display: grid;
|
||
grid-template-columns: auto 1fr;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-height: 54px;
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
background: rgba(245, 247, 251, 0.98);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
.apt-h5-back {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
height: 36px;
|
||
padding: 0 8px 0 4px;
|
||
border: 0;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: #2563eb;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.apt-h5-back span {
|
||
font-size: 26px;
|
||
line-height: 1;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.apt-h5-detail-title {
|
||
min-width: 0;
|
||
}
|
||
|
||
.apt-h5-detail-title strong,
|
||
.apt-h5-detail-title small {
|
||
display: block;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.apt-h5-detail-title strong {
|
||
color: #111827;
|
||
font-size: 16px;
|
||
line-height: 22px;
|
||
}
|
||
|
||
.apt-h5-detail-title small {
|
||
margin-top: 1px;
|
||
color: #8a94a6;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.apt-h5-detail-page {
|
||
min-height: calc(100vh - 54px);
|
||
padding: 12px 12px 82px;
|
||
}
|
||
|
||
.apt-h5-detail-section {
|
||
margin-bottom: 12px;
|
||
padding: 12px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
.apt-h5-detail-section h3 {
|
||
margin: 0 0 10px;
|
||
color: #111827;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.apt-h5-detail-section p {
|
||
margin: 0 0 8px;
|
||
color: #4b5563;
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.apt-h5-detail-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
}
|
||
|
||
.apt-h5-detail-grid div {
|
||
min-width: 0;
|
||
}
|
||
|
||
.apt-h5-detail-grid .apt-h5-detail-full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.apt-h5-detail-grid span {
|
||
display: block;
|
||
margin-bottom: 3px;
|
||
color: #8a94a6;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.apt-h5-detail-grid strong {
|
||
display: block;
|
||
color: #374151;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.apt-h5-detail-footer {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
width: 100%;
|
||
}
|
||
|
||
.apt-h5-detail-footer-bar {
|
||
position: sticky;
|
||
bottom: 0;
|
||
z-index: 20;
|
||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||
border-top: 1px solid #e5e7eb;
|
||
background: rgba(255, 255, 255, 0.98);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
.apt-h5-detail-footer :deep(.el-button) {
|
||
min-width: 0;
|
||
margin: 0;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.apt-h5-qr {
|
||
min-height: 160px;
|
||
text-align: center;
|
||
}
|
||
|
||
.apt-h5-qr-loading,
|
||
.apt-h5-qr-fail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 160px;
|
||
color: #6b7280;
|
||
gap: 10px;
|
||
}
|
||
|
||
.apt-h5-qr-img {
|
||
width: 220px;
|
||
height: 220px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.apt-h5-qr-meta {
|
||
margin-top: 10px;
|
||
color: #6b7280;
|
||
font-size: 13px;
|
||
line-height: 22px;
|
||
}
|
||
|
||
.apt-h5-complete p {
|
||
margin: 0 0 12px;
|
||
color: #374151;
|
||
font-size: 14px;
|
||
line-height: 22px;
|
||
}
|
||
|
||
.apt-h5-complete :deep(.el-textarea__inner) {
|
||
min-height: 112px !important;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
@media (max-width: 374px) {
|
||
.apt-h5-card-foot {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.apt-h5-actions {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style lang="scss">
|
||
body.h5-mode {
|
||
background: #eef2f7;
|
||
|
||
.el-drawer {
|
||
width: min(100vw, 560px) !important;
|
||
max-width: 560px !important;
|
||
}
|
||
|
||
.el-drawer.rtl {
|
||
right: max(0px, calc((100vw - 560px) / 2)) !important;
|
||
}
|
||
|
||
.el-drawer.ltr {
|
||
left: max(0px, calc((100vw - 560px) / 2)) !important;
|
||
}
|
||
|
||
.el-drawer.btt,
|
||
.el-drawer.ttb {
|
||
left: 50% !important;
|
||
right: auto !important;
|
||
transform: translateX(-50%) !important;
|
||
}
|
||
|
||
.el-drawer__body {
|
||
padding: 0 14px 14px !important;
|
||
overflow-y: auto !important;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.el-drawer__header {
|
||
padding: 14px 16px !important;
|
||
margin-bottom: 0 !important;
|
||
border-bottom: 1px solid #eee;
|
||
background: #fff;
|
||
|
||
.el-drawer__title {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.el-drawer__footer {
|
||
padding: 10px 16px !important;
|
||
border-top: 1px solid #eee;
|
||
background: #fff;
|
||
}
|
||
|
||
.el-overlay-dialog .el-dialog,
|
||
.el-dialog {
|
||
width: 92% !important;
|
||
max-width: 460px !important;
|
||
margin: 8vh auto !important;
|
||
border-radius: 12px !important;
|
||
}
|
||
|
||
.el-dialog__body {
|
||
max-height: 65vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.edit-drawer {
|
||
.el-row {
|
||
display: block !important;
|
||
margin-left: 0 !important;
|
||
margin-right: 0 !important;
|
||
}
|
||
|
||
.el-col {
|
||
max-width: 100% !important;
|
||
flex: 0 0 100% !important;
|
||
width: 100% !important;
|
||
padding-left: 0 !important;
|
||
padding-right: 0 !important;
|
||
}
|
||
}
|
||
|
||
.diagnosis-drawer {
|
||
.el-drawer__body { padding-bottom: 60px !important; }
|
||
|
||
.el-form {
|
||
padding-bottom: 10px;
|
||
|
||
.el-form-item {
|
||
flex-direction: column !important;
|
||
align-items: flex-start !important;
|
||
margin-bottom: 10px !important;
|
||
padding: 0 !important;
|
||
|
||
.el-form-item__label {
|
||
width: auto !important;
|
||
max-width: 100% !important;
|
||
min-width: 0 !important;
|
||
text-align: left !important;
|
||
padding: 0 0 3px 0 !important;
|
||
font-size: 14px !important;
|
||
font-weight: 500;
|
||
color: #475569;
|
||
line-height: 1.4 !important;
|
||
height: auto !important;
|
||
justify-content: flex-start !important;
|
||
}
|
||
|
||
.el-form-item__content {
|
||
margin-left: 0 !important;
|
||
width: 100% !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.el-divider {
|
||
margin: 18px 0 12px !important;
|
||
|
||
.el-divider__text {
|
||
max-width: calc(100% - 32px);
|
||
padding: 0 8px !important;
|
||
color: #0f172a;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.checkbox-form-item {
|
||
margin-bottom: 14px !important;
|
||
}
|
||
|
||
.el-radio-group,
|
||
.el-checkbox-group {
|
||
display: grid !important;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 8px;
|
||
width: 100%;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.checkbox-form-item .el-checkbox-group {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
.el-radio,
|
||
.el-checkbox,
|
||
.el-radio-button,
|
||
.el-checkbox-button {
|
||
width: 100%;
|
||
height: auto !important;
|
||
margin: 0 !important;
|
||
white-space: normal !important;
|
||
}
|
||
|
||
.el-radio__label,
|
||
.el-checkbox__label {
|
||
white-space: normal !important;
|
||
line-height: 18px !important;
|
||
}
|
||
|
||
.el-radio-button__inner,
|
||
.el-checkbox-button__inner {
|
||
display: flex !important;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100% !important;
|
||
min-height: 34px;
|
||
padding: 7px 8px !important;
|
||
border: 1px solid #d8dee8 !important;
|
||
border-radius: 8px !important;
|
||
box-shadow: none !important;
|
||
white-space: normal !important;
|
||
line-height: 18px !important;
|
||
text-align: center;
|
||
}
|
||
|
||
.el-radio-button:first-child .el-radio-button__inner,
|
||
.el-checkbox-button:first-child .el-checkbox-button__inner,
|
||
.el-radio-button:last-child .el-radio-button__inner,
|
||
.el-checkbox-button:last-child .el-checkbox-button__inner {
|
||
border-radius: 8px !important;
|
||
}
|
||
|
||
.el-radio-button.is-active .el-radio-button__inner,
|
||
.el-checkbox-button.is-checked .el-checkbox-button__inner {
|
||
border-color: #2563eb !important;
|
||
background: #2563eb !important;
|
||
color: #fff !important;
|
||
}
|
||
|
||
@media (max-width: 430px) {
|
||
.checkbox-form-item .el-checkbox-group {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
}
|
||
|
||
.tcm-diagnosis-tabs {
|
||
.el-tabs__header {
|
||
margin: 0 0 10px !important;
|
||
}
|
||
|
||
.el-tabs__nav-wrap {
|
||
overflow-x: auto;
|
||
}
|
||
}
|
||
|
||
.tcm-dd-header__title {
|
||
font-size: 17px !important;
|
||
line-height: 24px !important;
|
||
}
|
||
|
||
.prescription-edit,
|
||
.prescription-preview-content {
|
||
max-width: 100%;
|
||
overflow-x: hidden;
|
||
|
||
.el-row {
|
||
display: block !important;
|
||
margin-left: 0 !important;
|
||
margin-right: 0 !important;
|
||
}
|
||
|
||
.el-col {
|
||
max-width: 100% !important;
|
||
flex: 0 0 100% !important;
|
||
width: 100% !important;
|
||
padding-left: 0 !important;
|
||
padding-right: 0 !important;
|
||
}
|
||
|
||
.el-form-item {
|
||
display: block !important;
|
||
margin-bottom: 10px !important;
|
||
|
||
.el-form-item__label {
|
||
display: block !important;
|
||
width: auto !important;
|
||
height: auto !important;
|
||
padding: 0 0 4px !important;
|
||
line-height: 1.4 !important;
|
||
text-align: left !important;
|
||
}
|
||
|
||
.el-form-item__content {
|
||
margin-left: 0 !important;
|
||
width: 100% !important;
|
||
}
|
||
}
|
||
|
||
.info-section {
|
||
padding: 12px !important;
|
||
margin-bottom: 12px !important;
|
||
border-radius: 8px !important;
|
||
}
|
||
|
||
.rp-toolbar,
|
||
.rp-toolbar__lead,
|
||
.rp-toolbar__actions,
|
||
.herb-editor-card__row,
|
||
.usage-formula-row {
|
||
display: block !important;
|
||
}
|
||
|
||
.rp-toolbar__actions {
|
||
margin-top: 10px;
|
||
|
||
.el-button {
|
||
width: 100%;
|
||
margin: 0 0 8px !important;
|
||
}
|
||
}
|
||
|
||
.herbs-editor-grid {
|
||
display: grid !important;
|
||
grid-template-columns: 1fr !important;
|
||
gap: 8px !important;
|
||
}
|
||
|
||
.herb-editor-card,
|
||
.herb-editor-card__select,
|
||
.herb-editor-card__dose,
|
||
.el-input-number,
|
||
.el-select,
|
||
.el-input,
|
||
.el-textarea {
|
||
width: 100% !important;
|
||
max-width: 100% !important;
|
||
}
|
||
|
||
.herb-editor-card__row {
|
||
.el-button {
|
||
margin-top: 6px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.chat-floating-window {
|
||
position: fixed !important;
|
||
right: max(0px, calc((100vw - 560px) / 2)) !important;
|
||
bottom: 0 !important;
|
||
left: auto !important;
|
||
top: auto !important;
|
||
width: min(100vw, 560px) !important;
|
||
height: min(76vh, 680px) !important;
|
||
max-width: 560px !important;
|
||
border-radius: 12px 12px 0 0 !important;
|
||
transform: none !important;
|
||
z-index: 3000 !important;
|
||
}
|
||
|
||
.chat-floating-window--minimized {
|
||
height: 48px !important;
|
||
}
|
||
|
||
.chat-window-header {
|
||
min-height: 48px;
|
||
padding: 0 10px !important;
|
||
}
|
||
|
||
.chat-window-title {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.chat-container,
|
||
.chat-content,
|
||
.chat-layout,
|
||
.chat-area {
|
||
width: 100% !important;
|
||
max-width: 100% !important;
|
||
min-width: 0 !important;
|
||
}
|
||
|
||
.chat-container {
|
||
height: calc(76vh - 48px) !important;
|
||
}
|
||
|
||
.chat-dialog-call-kit-wrapper {
|
||
left: 50% !important;
|
||
right: auto !important;
|
||
top: 8px !important;
|
||
width: min(100vw, 560px) !important;
|
||
max-width: 560px !important;
|
||
transform: translateX(-50%) !important;
|
||
}
|
||
|
||
.call-kit-content {
|
||
width: 100% !important;
|
||
max-width: 100vw !important;
|
||
overflow: hidden;
|
||
}
|
||
|
||
@media (max-width: 560px) {
|
||
.el-drawer {
|
||
width: 100% !important;
|
||
max-width: 100vw !important;
|
||
}
|
||
|
||
.el-drawer.rtl {
|
||
right: 0 !important;
|
||
}
|
||
|
||
.el-drawer.ltr,
|
||
.el-drawer.btt,
|
||
.el-drawer.ttb {
|
||
left: 0 !important;
|
||
transform: none !important;
|
||
}
|
||
|
||
.chat-floating-window {
|
||
right: 0 !important;
|
||
width: 100vw !important;
|
||
max-width: 100vw !important;
|
||
}
|
||
|
||
.chat-dialog-call-kit-wrapper {
|
||
width: 100vw !important;
|
||
max-width: 100vw !important;
|
||
}
|
||
}
|
||
}
|
||
</style>
|