Files
zyt/admin/src/views/first_visit/my_patients/index.vue
T
2026-08-07 09:16:15 +08:00

1139 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="my-patients-page">
<div class="page-heading">
<div>
<div class="eyebrow">一诊</div>
<h1>我的患者</h1>
<p>患者挂号与诊单信息按当前角色和部门数据范围展示</p>
</div>
<div class="heading-actions">
<el-tag effect="plain" round>{{ scopeLabel }}</el-tag>
<el-button :icon="Refresh" :loading="workspaceLoading" @click="refreshPage">刷新</el-button>
</div>
</div>
<el-card class="workspace-card" shadow="never">
<el-tabs v-model="activeWorkspace" class="workspace-tabs">
<el-tab-pane label="患者列表" name="patients" />
<el-tab-pane label="订单管理" name="orders" />
<el-tab-pane label="面诊进度" name="progress" />
</el-tabs>
<div v-show="activeWorkspace === 'patients'">
<div class="filter-panel">
<div class="filter-row filter-row-main">
<el-input
v-model="formData.keyword"
class="keyword-input"
clearable
:prefix-icon="Search"
placeholder="患者姓名 / 手机号 / 助理 / 医生"
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
<div class="filter-group status-group">
<span class="filter-label">状态</span>
<el-radio-group v-model="formData.status_filter" @change="handleFilterChange">
<el-radio-button v-for="item in statusOptions" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
</div>
<div class="filter-row date-filter-row">
<span class="filter-label">挂号时间</span>
<div class="quick-dates">
<el-button
v-for="item in dateOptions"
:key="item.value"
:type="activeDateType === item.value ? 'primary' : 'default'"
@click="selectDateType(item.value)"
>
{{ item.label }}
</el-button>
</div>
<el-date-picker
v-model="customDateRange"
class="date-range"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
:clearable="true"
@change="handleCustomDateChange"
/>
</div>
</div>
<div class="summary-grid">
<button class="summary-card summary-today" type="button" @click="selectDateType('today')">
<span class="summary-icon"><el-icon><Calendar /></el-icon></span>
<span class="summary-copy"><small>今日挂号</small><strong>{{ summary.today }}</strong><em></em></span>
<span class="summary-date">{{ summaryDates.today || '—' }}</span>
</button>
<button class="summary-card summary-tomorrow" type="button" @click="selectDateType('tomorrow')">
<span class="summary-icon"><el-icon><Clock /></el-icon></span>
<span class="summary-copy"><small>明日预约</small><strong>{{ summary.tomorrow }}</strong><em></em></span>
<span class="summary-date">{{ summaryDates.tomorrow || '—' }}</span>
</button>
<button class="summary-card summary-after" type="button" @click="selectDateType('day_after')">
<span class="summary-icon"><el-icon><Calendar /></el-icon></span>
<span class="summary-copy"><small>后天预约</small><strong>{{ summary.day_after }}</strong><em></em></span>
<span class="summary-date">{{ summaryDates.day_after || '—' }}</span>
</button>
</div>
<div class="table-heading">
<div>
<h2>患者列表</h2>
<span> {{ pager.count }} 位患者</span>
</div>
<span class="table-scope"><el-icon><Lock /></el-icon>{{ scopeLabel }}</span>
</div>
<el-table
v-loading="pager.loading"
:data="pager.lists"
class="patient-table"
:row-class-name="tableRowClassName"
@row-dblclick="openDiagnosis"
>
<el-table-column label="患者" min-width="190" fixed="left">
<template #default="{ row }">
<div class="patient-cell">
<span class="patient-avatar">{{ patientInitial(row.patient_name) }}</span>
<div>
<strong>{{ row.patient_name || '未命名患者' }}</strong>
<p>{{ row.gender_desc }} · {{ row.age || '—' }} · {{ row.phone_masked || '无手机号' }}</p>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="assistant_name" label="归属助理" min-width="120" />
<el-table-column label="预约医生" min-width="150">
<template #default="{ row }">
<div class="doctor-cell">
<span>{{ row.appointment_doctor_name || '未预约' }}</span>
<el-tag v-if="row.appointment_status" size="small" :type="appointmentTagType(row.appointment_status)" effect="light">
{{ row.appointment_status_text }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="预约时间" min-width="165">
<template #default="{ row }">
<span :class="['appointment-time', { empty: !row.appointment_time_text }]">
{{ row.appointment_time_text || '暂无预约' }}
</span>
</template>
</el-table-column>
<el-table-column label="复诊次数" width="105" align="center">
<template #default="{ row }">
<span class="revisit-count">{{ row.revisit_count || 0 }} </span>
</template>
</el-table-column>
<el-table-column label="确认信息" width="110" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.confirmed ? 'success' : 'warning'" effect="light">
{{ row.confirmation_text }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="诊单日期" width="118">
<template #default="{ row }">{{ row.diagnosis_date_text || '—' }}</template>
</el-table-column>
<el-table-column label="操作" min-width="430" fixed="right">
<template #default="{ row }">
<div class="row-actions">
<el-button v-if="canEditDiagnosis" type="primary" link @click="openDiagnosis(row)">诊单</el-button>
<el-button v-else-if="canReadDiagnosis" type="primary" link @click="openReadonlyDiagnosis(row)">查看</el-button>
<el-button v-if="canBookAppointment" type="primary" link @click="openAppointment(row)">预约</el-button>
<el-button
v-if="canAssignPatient"
type="warning"
link
@click="openAssignDialog(row)"
>
{{ Number(row.assistant_id) > 0 ? '重新指派' : '指派' }}
</el-button>
<el-button
v-if="canFillIdCard && !Number(row.has_id_card)"
type="warning"
link
@click="openFillIdCardDialog(row)"
>
补全身份证
</el-button>
<el-button
v-if="canShowDiagnosisQRCode(row)"
type="primary"
link
@click="openDiagnosisQRCode(row)"
>
诊单二维码
</el-button>
<el-button
v-if="canBookAppointment && canCancelAppointment(row)"
type="danger"
link
@click="cancelAppointmentForRow(row)"
>
取消挂号
</el-button>
</div>
</template>
</el-table-column>
<template #empty>
<el-empty description="当前范围内暂无患者" />
</template>
</el-table>
<div class="pagination-wrap">
<pagination v-model="pager" @change="getLists" />
</div>
</div>
<order-panel
v-if="activeWorkspace === 'orders'"
ref="orderPanelRef"
@open-diagnosis="openDiagnosis"
/>
<progress-panel
v-if="activeWorkspace === 'progress'"
ref="progressPanelRef"
@open-diagnosis="openDiagnosis"
/>
</el-card>
<edit-popup ref="editRef" @success="refreshPage" />
<appointment-popup ref="appointmentRef" api-scene="my_patient" @success="refreshPage" />
<el-dialog
v-model="assignDialogVisible"
title="指派医助"
width="500px"
:close-on-click-modal="false"
>
<el-form :model="assignForm" label-width="88px">
<el-form-item label="患者">
<span class="dialog-patient-name">{{ currentActionPatient?.patient_name || '—' }}</span>
</el-form-item>
<el-form-item label="当前助理">
<span>{{ currentActionPatient?.assistant_name || '未分配' }}</span>
</el-form-item>
<el-form-item label="选择医助" required>
<el-select
v-model="assignForm.assistant_id"
class="dialog-full-width"
filterable
clearable
:loading="assistantOptionsLoading"
placeholder="请选择当前范围内的医助"
>
<el-option
v-for="item in assistantOptions"
:key="item.id"
:label="assistantOptionLabel(item)"
:value="Number(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item label="继承">
<div class="inherit-field">
<el-checkbox v-model="assignForm.is_inherit">作为继承指派</el-checkbox>
<small>用于区分继承关系指派操作仍会完整记录</small>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="assignDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="assignLoading" @click="submitAssign">确定指派</el-button>
</template>
</el-dialog>
<el-dialog
v-model="fillIdCardDialogVisible"
title="补全身份证号"
width="430px"
:close-on-click-modal="false"
@closed="resetFillIdCardForm"
>
<el-form
ref="fillIdCardFormRef"
:model="fillIdCardForm"
:rules="fillIdCardRules"
label-width="88px"
>
<el-form-item label="患者">
<span class="dialog-patient-name">{{ fillIdCardForm.patient_name || '—' }}</span>
</el-form-item>
<el-form-item label="身份证号" prop="id_card">
<el-input
v-model="fillIdCardForm.id_card"
maxlength="18"
clearable
show-word-limit
placeholder="请输入15或18位身份证号"
@keyup.enter="submitFillIdCard"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="fillIdCardDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="fillIdCardLoading" @click="submitFillIdCard">
提交并更新年龄
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="qrcodeDialogVisible"
title="诊单二维码"
width="400px"
:close-on-click-modal="false"
>
<div class="qrcode-body">
<template v-if="qrcodeLoading">
<el-icon class="is-loading" :size="40"><Loading /></el-icon>
<span>二维码生成中...</span>
</template>
<template v-else-if="qrcodeUrl">
<img :src="qrcodeUrl" alt="诊单二维码" class="qrcode-image" />
<div class="qrcode-meta">
<div>患者{{ currentQRCodePatient?.patient_name || '—' }}</div>
<div>挂号时间{{ currentQRCodePatient?.appointment_time_text || '—' }}</div>
<div>请使用企业微信扫描二维码然后转发给患者</div>
</div>
</template>
<span v-else class="qrcode-error">生成失败请重试</span>
</div>
<template #footer>
<el-button @click="qrcodeDialogVisible = false">关闭</el-button>
<el-button v-if="!qrcodeLoading" type="primary" @click="regenerateDiagnosisQRCode">重新生成</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, defineAsyncComponent, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import dayjs from 'dayjs'
import { Calendar, Clock, Loading, Lock, Refresh, Search } from '@element-plus/icons-vue'
import { usePaging } from '@/hooks/usePaging'
import { hasPermission } from '@/utils/perm'
import feedback from '@/utils/feedback'
import {
myPatientAssign,
myPatientAssistants,
myPatientCancelAppointment,
myPatientFillIdCard,
myPatientLists
} from '@/api/first_visit'
import { generateMiniProgramQrcode } from '@/api/tcm'
import { getWeappConfig } from '@/api/channel/weapp'
import useUserStore from '@/stores/modules/user'
const EditPopup = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
const AppointmentPopup = defineAsyncComponent(() => import('@/views/tcm/diagnosis/appointment.vue'))
const OrderPanel = defineAsyncComponent(() => import('./components/OrderPanel.vue'))
const ProgressPanel = defineAsyncComponent(() => import('./components/ProgressPanel.vue'))
type StatusFilter = '' | 'unbooked' | 'pending_interview' | 'completed' | 'missed'
type DateType = 'all' | 'today' | 'tomorrow' | 'day_after' | 'last7' | 'last30' | 'custom'
const router = useRouter()
const userStore = useUserStore()
const activeWorkspace = ref('patients')
const activeDateType = ref<DateType>('all')
const customDateRange = ref<string[]>([])
const editRef = ref<any>()
const appointmentRef = ref<any>()
const orderPanelRef = ref<any>()
const progressPanelRef = ref<any>()
const qrcodeDialogVisible = ref(false)
const qrcodeLoading = ref(false)
const qrcodeUrl = ref('')
const currentQRCodePatient = ref<any>(null)
const assignDialogVisible = ref(false)
const assignLoading = ref(false)
const assistantOptionsLoading = ref(false)
const assistantOptions = ref<any[]>([])
const currentActionPatient = ref<any>(null)
const assignForm = reactive({
assistant_id: null as number | null,
is_inherit: false
})
const fillIdCardDialogVisible = ref(false)
const fillIdCardLoading = ref(false)
const fillIdCardFormRef = ref<any>()
const fillIdCardForm = reactive({
id: 0,
patient_name: '',
id_card: ''
})
const formData = reactive({
keyword: '',
status_filter: '' as StatusFilter,
start_date: '',
end_date: ''
})
const statusOptions: Array<{ label: string; value: StatusFilter }> = [
{ label: '全部', value: '' },
{ label: '未预约', value: 'unbooked' },
{ label: '待面诊', value: 'pending_interview' },
{ label: '已完成', value: 'completed' },
{ label: '已过号', value: 'missed' }
]
const dateOptions: Array<{ label: string; value: DateType }> = [
{ label: '全部时间', value: 'all' },
{ label: '今日挂号', value: 'today' },
{ label: '明日', value: 'tomorrow' },
{ label: '后天', value: 'day_after' },
{ label: '近7天', value: 'last7' },
{ label: '近30天', value: 'last30' }
]
const { pager, getLists, resetPage } = usePaging({
fetchFun: myPatientLists as any,
params: formData,
size: 15,
firstLoading: true
})
const summary = computed(() => ({
today: Number(pager.extend?.summary?.today || 0),
tomorrow: Number(pager.extend?.summary?.tomorrow || 0),
day_after: Number(pager.extend?.summary?.day_after || 0)
}))
const summaryDates = computed(() => pager.extend?.dates || {})
const scopeLabel = computed(() => pager.extend?.scope?.label || '按权限加载')
const canEditDiagnosis = computed(() => hasPermission(['tcm.diagnosis/edit']))
const canReadDiagnosis = computed(() => hasPermission(['tcm.diagnosis/readonlyDetail']))
const canBookAppointment = computed(() => hasPermission(['tcm.diagnosis/guahao']))
const canAssignPatient = computed(() => hasPermission(['tcm.diagnosis/assign']))
const canFillIdCard = computed(() => hasPermission(['tcm.diagnosis/edit']))
const workspaceLoading = computed(() => {
if (activeWorkspace.value === 'orders') return Boolean(orderPanelRef.value?.loading)
if (activeWorkspace.value === 'progress') return Boolean(progressPanelRef.value?.loading)
return pager.loading
})
function handleSearch() {
resetPage()
}
function handleFilterChange() {
resetPage()
}
function refreshPage() {
if (activeWorkspace.value === 'orders') return orderPanelRef.value?.refresh?.()
if (activeWorkspace.value === 'progress') return progressPanelRef.value?.refresh?.()
return getLists()
}
function resetFilters() {
formData.keyword = ''
formData.status_filter = ''
formData.start_date = ''
formData.end_date = ''
activeDateType.value = 'all'
customDateRange.value = []
resetPage()
}
function selectDateType(type: DateType) {
activeDateType.value = type
customDateRange.value = []
const today = dayjs()
if (type === 'all') {
formData.start_date = ''
formData.end_date = ''
} else if (type === 'today') {
formData.start_date = today.format('YYYY-MM-DD')
formData.end_date = formData.start_date
} else if (type === 'tomorrow') {
formData.start_date = today.add(1, 'day').format('YYYY-MM-DD')
formData.end_date = formData.start_date
} else if (type === 'day_after') {
formData.start_date = today.add(2, 'day').format('YYYY-MM-DD')
formData.end_date = formData.start_date
} else if (type === 'last7') {
formData.start_date = today.subtract(6, 'day').format('YYYY-MM-DD')
formData.end_date = today.format('YYYY-MM-DD')
} else if (type === 'last30') {
formData.start_date = today.subtract(29, 'day').format('YYYY-MM-DD')
formData.end_date = today.format('YYYY-MM-DD')
}
resetPage()
}
function handleCustomDateChange(value: string[] | null) {
if (!value?.length) {
activeDateType.value = 'all'
formData.start_date = ''
formData.end_date = ''
} else {
activeDateType.value = 'custom'
formData.start_date = value[0] || ''
formData.end_date = value[1] || value[0] || ''
}
resetPage()
}
function openDiagnosis(row: any) {
if (!canEditDiagnosis.value) {
openReadonlyDiagnosis(row)
return
}
editRef.value?.open?.('edit', Number(row.diagnosis_id || row.id))
}
function openReadonlyDiagnosis(row: any) {
if (!canReadDiagnosis.value) {
feedback.msgWarning('当前角色没有诊单查看权限')
return
}
const route = router.getRoutes().find((item) => item.meta?.perms === 'tcm.diagnosis/readonlyDetail')
if (route?.path) {
router.push({ path: route.path, query: { id: String(row.diagnosis_id || row.id) } })
} else {
router.push({ path: '/tcm/diagnosis-readonly', query: { id: String(row.diagnosis_id || row.id) } })
}
}
function openAppointment(row: any) {
appointmentRef.value?.open?.({
...row,
id: Number(row.diagnosis_id || row.id),
patient_id: Number(row.diagnosis_id || row.id)
})
}
function assistantOptionLabel(item: any) {
const name = String(item?.name || item?.account || `医助${item?.id || ''}`)
const departments = String(item?.dept_names || '').trim()
return departments ? `${name} · ${departments}` : name
}
async function loadAssistantOptions() {
assistantOptionsLoading.value = true
try {
const result = await myPatientAssistants()
assistantOptions.value = Array.isArray(result) ? result : []
} catch (error: any) {
assistantOptions.value = []
feedback.msgError(error?.msg || '医助列表加载失败')
} finally {
assistantOptionsLoading.value = false
}
}
async function openAssignDialog(row: any) {
currentActionPatient.value = row
assignForm.assistant_id = Number(row.assistant_id) > 0 ? Number(row.assistant_id) : null
assignForm.is_inherit = false
assignDialogVisible.value = true
if (assistantOptions.value.length === 0) {
await loadAssistantOptions()
}
}
async function submitAssign() {
const diagnosisId = Number(currentActionPatient.value?.diagnosis_id || currentActionPatient.value?.id)
if (diagnosisId <= 0) {
feedback.msgWarning('患者诊单信息不完整')
return
}
if (!assignForm.assistant_id) {
feedback.msgWarning('请选择医助')
return
}
assignLoading.value = true
try {
await myPatientAssign({
id: diagnosisId,
assistant_id: Number(assignForm.assistant_id),
is_inherit: assignForm.is_inherit ? 1 : 0
})
feedback.msgSuccess('指派成功')
assignDialogVisible.value = false
await getLists()
} catch (error: any) {
feedback.msgError(error?.msg || '指派失败')
} finally {
assignLoading.value = false
}
}
const validateIdCard = (_rule: any, value: string, callback: (error?: Error) => void) => {
const idCard = String(value || '').trim()
if (!idCard) {
callback(new Error('请输入身份证号'))
return
}
const valid18 = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(idCard)
const valid15 = /^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$/.test(idCard)
callback(valid18 || valid15 ? undefined : new Error('请输入15或18位有效身份证号'))
}
const fillIdCardRules = {
id_card: [{ required: true, validator: validateIdCard, trigger: 'blur' }]
}
function openFillIdCardDialog(row: any) {
fillIdCardForm.id = Number(row.diagnosis_id || row.id)
fillIdCardForm.patient_name = String(row.patient_name || '')
fillIdCardForm.id_card = ''
fillIdCardDialogVisible.value = true
}
function resetFillIdCardForm() {
fillIdCardForm.id = 0
fillIdCardForm.patient_name = ''
fillIdCardForm.id_card = ''
fillIdCardFormRef.value?.clearValidate?.()
}
async function submitFillIdCard() {
if (fillIdCardLoading.value) return
try {
await fillIdCardFormRef.value?.validate?.()
} catch {
return
}
fillIdCardLoading.value = true
try {
await myPatientFillIdCard({
id: fillIdCardForm.id,
id_card: fillIdCardForm.id_card.trim()
})
feedback.msgSuccess('补全成功,年龄已自动更新')
fillIdCardDialogVisible.value = false
await getLists()
} catch (error: any) {
feedback.msgError(error?.msg || '补全身份证失败')
} finally {
fillIdCardLoading.value = false
}
}
function canShowDiagnosisQRCode(row: any) {
return canBookAppointment.value
&& Number(row.appointment_id) > 0
&& Number(row.appointment_status) === 1
&& Number(row.appointment_doctor_id) > 0
}
async function openDiagnosisQRCode(row: any) {
if (!canShowDiagnosisQRCode(row)) {
feedback.msgWarning('仅已挂号患者可以生成诊单二维码')
return
}
const diagnosisId = Number(row.diagnosis_id || row.id)
const patientId = Number(row.source_patient_id || row.diagnosis_id || row.id)
if (diagnosisId <= 0 || patientId <= 0) {
feedback.msgWarning('患者诊单信息不完整')
return
}
currentQRCodePatient.value = row
qrcodeDialogVisible.value = true
qrcodeLoading.value = true
qrcodeUrl.value = ''
try {
const config = await getWeappConfig()
if (!config?.app_id) {
throw new Error('小程序未配置,请先配置小程序信息')
}
const result = await generateMiniProgramQrcode({
diagnosis_id: diagnosisId,
doctor_id: Number(row.appointment_doctor_id),
patient_id: patientId,
share_user_id: userStore.userInfo?.id || ''
})
if (!result?.qrcode_url) {
throw new Error('二维码生成失败')
}
qrcodeUrl.value = result.qrcode_url
} catch (error: any) {
feedback.msgError(error?.msg || error?.message || '生成二维码失败,请重试')
} finally {
qrcodeLoading.value = false
}
}
function regenerateDiagnosisQRCode() {
if (currentQRCodePatient.value) {
openDiagnosisQRCode(currentQRCodePatient.value)
}
}
function canCancelAppointment(row: any) {
return Number(row.appointment_id) > 0 && [1, 4].includes(Number(row.appointment_status))
}
async function cancelAppointmentForRow(row: any) {
if (!canCancelAppointment(row)) return
try {
await feedback.confirm(`确定取消“${row.patient_name || '该患者'}”的挂号吗?`)
await myPatientCancelAppointment({ id: Number(row.appointment_id) })
feedback.msgSuccess('取消挂号成功')
await refreshPage()
} catch (error: any) {
if (error !== 'cancel') {
feedback.msgError(error?.msg || '取消挂号失败')
}
}
}
function patientInitial(name: string) {
const text = String(name || '患').trim()
return text.slice(0, 1)
}
function appointmentTagType(status: number): 'primary' | 'success' | 'warning' | 'info' {
return ({ 1: 'primary', 3: 'success', 4: 'warning' } as const)[Number(status) as 1 | 3 | 4] || 'info'
}
function tableRowClassName({ row }: { row: any }) {
if (!row.confirmed) return 'patient-row-unconfirmed'
if (Number(row.appointment_status) === 4) return 'patient-row-missed'
return ''
}
onMounted(() => {
getLists()
})
</script>
<style scoped lang="scss">
.my-patients-page {
min-height: calc(100vh - 90px);
padding: 18px;
background: #f5f7fa;
color: #172033;
}
.qrcode-body {
display: flex;
min-height: 320px;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 16px;
color: #7b8798;
}
.qrcode-image {
width: 256px;
height: 256px;
border: 1px solid #e1e7ed;
border-radius: 10px;
object-fit: contain;
}
.qrcode-meta {
display: grid;
gap: 6px;
color: #697587;
text-align: center;
font-size: 12px;
}
.qrcode-error {
color: var(--el-color-danger);
}
.page-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 16px;
.eyebrow {
margin-bottom: 3px;
color: #0f9185;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
}
h1 {
margin: 0;
font-size: 24px;
line-height: 1.3;
font-weight: 700;
}
p {
margin: 5px 0 0;
color: #8490a3;
font-size: 13px;
}
}
.heading-actions {
display: flex;
align-items: center;
gap: 10px;
:deep(.el-tag) {
color: #0f766e;
border-color: #a7ded8;
background: #effbf9;
}
}
.workspace-card {
border: 1px solid #e3e8ef;
border-radius: 12px;
:deep(.el-card__body) {
padding: 0 16px 16px;
}
}
.workspace-tabs {
:deep(.el-tabs__header) {
margin: 0 -16px 16px;
padding: 0 16px;
background: #fbfcfd;
border-bottom: 1px solid #e9edf2;
}
:deep(.el-tabs__nav-wrap::after) {
display: none;
}
:deep(.el-tabs__item) {
height: 50px;
padding: 0 24px;
font-weight: 600;
}
:deep(.el-tabs__active-bar) {
height: 3px;
background: #0f9185;
}
}
.filter-panel {
padding: 14px;
border: 1px solid #e3e8ef;
border-radius: 10px;
background: #fbfcfd;
}
.filter-row {
display: flex;
align-items: center;
gap: 10px;
}
.filter-row-main {
flex-wrap: wrap;
}
.keyword-input {
width: min(390px, 100%);
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
flex: 0 0 auto;
color: #5d687a;
font-size: 13px;
font-weight: 600;
}
.date-filter-row {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e8edf2;
flex-wrap: wrap;
}
.quick-dates {
display: flex;
flex-wrap: wrap;
gap: 7px;
:deep(.el-button + .el-button) {
margin-left: 0;
}
}
.date-range {
width: 280px;
margin-left: auto;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin: 14px 0;
}
.summary-card {
position: relative;
display: flex;
align-items: center;
min-height: 96px;
padding: 16px;
text-align: left;
border: 1px solid #dfe6ee;
border-radius: 10px;
background: #fff;
cursor: pointer;
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
&:hover {
border-color: #82cfc7;
box-shadow: 0 8px 20px rgba(28, 74, 70, 0.08);
transform: translateY(-1px);
}
}
.summary-icon {
display: grid;
width: 42px;
height: 42px;
margin-right: 13px;
place-items: center;
border-radius: 10px;
color: #0f9185;
background: #e8f7f5;
font-size: 20px;
}
.summary-copy {
display: flex;
align-items: baseline;
gap: 5px;
small {
position: absolute;
top: 16px;
color: #718096;
font-size: 13px;
}
strong {
margin-top: 20px;
font-size: 28px;
line-height: 1;
}
em {
color: #667085;
font-size: 12px;
font-style: normal;
}
}
.summary-date {
margin-left: auto;
color: #98a2b3;
font-size: 12px;
}
.summary-tomorrow .summary-icon {
color: #3978d6;
background: #edf4ff;
}
.summary-after .summary-icon {
color: #c27728;
background: #fff4e8;
}
.table-heading {
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0 10px;
> div {
display: flex;
align-items: baseline;
gap: 10px;
}
h2 {
margin: 0;
font-size: 16px;
}
span {
color: #8a95a6;
font-size: 12px;
}
}
.table-scope {
display: inline-flex;
align-items: center;
gap: 5px;
}
.patient-table {
width: 100%;
border: 1px solid #e7ebf0;
border-radius: 9px;
overflow: hidden;
:deep(th.el-table__cell) {
height: 44px;
color: #5f6b7d;
background: #f7f9fb;
font-weight: 600;
}
:deep(td.el-table__cell) {
padding: 10px 0;
}
:deep(.patient-row-unconfirmed > td.el-table__cell) {
background: #fffaf0;
}
:deep(.patient-row-missed > td.el-table__cell) {
background: #fffdf8;
}
}
.patient-cell {
display: flex;
align-items: center;
gap: 10px;
strong {
display: block;
color: #202939;
font-size: 14px;
}
p {
margin: 4px 0 0;
color: #8b96a8;
font-size: 12px;
}
}
.patient-avatar {
display: grid;
flex: 0 0 36px;
width: 36px;
height: 36px;
place-items: center;
border-radius: 10px;
color: #0f766e;
background: #e5f5f2;
font-weight: 700;
}
.doctor-cell {
display: flex;
align-items: center;
gap: 7px;
}
.appointment-time {
color: #344054;
&.empty {
color: #98a2b3;
}
}
.revisit-count {
color: #475467;
font-variant-numeric: tabular-nums;
}
.row-actions {
display: flex;
align-items: center;
white-space: nowrap;
:deep(.el-button + .el-button) {
margin-left: 10px;
}
}
.dialog-full-width {
width: 100%;
}
.dialog-patient-name {
color: #1f2937;
font-weight: 600;
}
.inherit-field {
display: grid;
gap: 2px;
small {
color: #98a2b3;
line-height: 1.5;
}
}
.pagination-wrap {
display: flex;
justify-content: flex-end;
padding-top: 16px;
}
@media (max-width: 1180px) {
.summary-grid {
grid-template-columns: 1fr;
}
.date-range {
width: 100%;
margin-left: 0;
}
}
@media (max-width: 760px) {
.my-patients-page {
padding: 12px;
}
.page-heading {
align-items: flex-start;
flex-direction: column;
}
.filter-row,
.filter-group {
align-items: stretch;
flex-direction: column;
}
.status-group :deep(.el-radio-group) {
display: flex;
flex-wrap: wrap;
}
}
</style>