ykt-wxapp/pages/case/case.vue

1011 lines
28 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>
<view class="container">
<!-- Header -->
<view class="header">
<view class="team-selector" @click="toggleTeamPopup">
<text class="team-name">{{ teamDisplay }}</text>
<uni-icons type="loop" size="18" color="#333" class="team-icon"></uni-icons>
</view>
<view class="header-actions">
<view class="action-item" @click="goToSearch">
<uni-icons type="search" size="22" color="#333"></uni-icons>
<text class="action-text">搜索</text>
</view>
<view class="action-item" @click="toggleBatchMode">
<uni-icons type="checkbox" size="22" color="#333"></uni-icons>
<text class="action-text">批量</text>
</view>
<view class="action-item" @click="goToGroupManage">
<uni-icons type="staff" size="22" color="#333"></uni-icons>
<text class="action-text">分组</text>
</view>
<view class="action-item" @click="handleCreate">
<uni-icons type="plusempty" size="22" color="#333"></uni-icons>
<text class="action-text">新增</text>
</view>
</view>
</view>
<!-- Tabs and Count -->
<view class="tabs-area">
<scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
<view class="tabs-container">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ active: currentTabKey === tab.key }"
@click="onTabClick(tab)"
>
{{ tab.label }}
</view>
</view>
</scroll-view>
<view class="total-count-inline">{{ totalPatients }}</view>
</view>
<!-- Main Content -->
<view class="content-body">
<!-- Patient List -->
<scroll-view
scroll-y
class="patient-list"
:scroll-into-view="scrollIntoId"
:scroll-with-animation="true"
lower-threshold="80"
@scrolltolower="loadMore"
>
<view v-for="group in patientList" :key="group.letter" :id="letterToDomId(group.letter)">
<view class="group-title">{{ group.letter }}</view>
<view v-for="(patient, pIndex) in group.data" :key="pIndex" class="patient-card" @click="handlePatientClick(patient)">
<!-- Checkbox for Batch Mode -->
<view v-if="isBatchMode" class="checkbox-area">
<uni-icons
:type="selectedItems.includes(getSelectId(patient)) ? 'checkbox-filled' : 'checkbox'"
size="24"
:color="selectedItems.includes(getSelectId(patient)) ? '#007aff' : '#ccc'"
></uni-icons>
</view>
<view class="card-content">
<!-- Row 1 -->
<view class="card-row-top">
<view class="patient-info">
<text class="patient-name">{{ patient.name }}</text>
<text class="patient-meta">{{ patient.gender }}/{{ patient.age }}</text>
</view>
<view class="patient-tags">
<view v-for="(tag, tIndex) in patient.tags" :key="tIndex" class="tag">
{{ tag }}
</view>
</view>
</view>
<!-- Row 2 / 3 -->
<view class="card-row-bottom">
<template v-if="currentTabKey === 'new'"> <!-- New Patient Tab -->
<text class="record-text">
{{ patient.createTime || '-' }} / {{ patient.creator || '-' }}
</text>
</template>
<template v-else>
<text v-if="patient.record" class="record-text">
{{ patient.record.type }} / {{ patient.record.date }} / {{ patient.record.diagnosis }}
</text>
<text v-else class="no-record">暂无病历记录</text>
</template>
</view>
</view>
</view>
</view>
<!-- Bottom padding for tabbar -->
<view style="height: 100px;"></view> <!-- Increased padding -->
</scroll-view>
<!-- Sidebar Index -->
<view v-if="!isBatchMode" class="sidebar-index">
<view
v-for="letter in indexList"
:key="letter"
class="index-item"
@click="scrollToLetter(letter)"
>
{{ letter }}
</view>
</view>
</view>
<!-- Batch Actions Footer -->
<view v-if="isBatchMode" class="batch-footer">
<view class="left-action" @click="handleSelectAll">
<uni-icons
:type="selectedItems.length > 0 && selectedItems.length === patientList.flatMap(g => g.data).length ? 'checkbox-filled' : 'checkbox'"
size="24"
color="#666"
></uni-icons>
<text class="footer-text">全选 ({{ selectedItems.length }})</text>
</view>
<view class="right-actions">
<button class="footer-btn plain" @click="cancelBatch">取消</button>
<button class="footer-btn primary" @click="handleTransfer">转移</button>
<button class="footer-btn primary" @click="handleShare">共享</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import dayjs from 'dayjs';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { toast } from '@/utils/widget';
// State
const teams = ref([]);
const currentTeam = ref(null);
const currentTabKey = ref('all');
const scrollIntoId = ref('');
const teamGroups = ref([]);
const tabs = computed(() => {
const base = [
{ key: 'all', label: '全部', kind: 'all' },
{ key: 'new', label: '新患者', kind: 'new' },
];
const groupTabs = (Array.isArray(teamGroups.value) ? teamGroups.value : [])
.filter((g) => g && g._id && g.groupName)
.map((g) => ({ key: `group:${g._id}`, label: String(g.groupName), kind: 'group', groupId: String(g._id) }));
return [...base, ...groupTabs];
});
const isBatchMode = ref(false);
const selectedItems = ref([]); // Stores patient phone or unique ID
// 新增流程所需状态(后续接接口替换)
const managedArchiveCountAllTeams = ref(0); // 在管档案数(所有团队)
const isVerified = ref(true); // 是否已认证
const hasVerifyFailedHistory = ref(false); // 是否有历史认证失败
const verifyFailedReason = ref('资料不完整,请补充营业执照/资质证明后重新提交。');
const DETAIL_STORAGE_KEY = 'ykt_case_archive_detail';
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
const GROUPS_RELOAD_KEY = 'ykt_case_groups_need_reload';
const BATCH_CUSTOMER_IDS_KEY = 'ykt_case_batch_customer_ids';
const page = ref(1);
const pages = ref(0);
const pageSize = ref(50);
const totalFromApi = ref(0);
const loading = ref(false);
const rawPatients = ref([]);
const more = computed(() => page.value < pages.value);
const currentTab = computed(() => tabs.value.find((t) => t.key === currentTabKey.value) || tabs.value[0]);
const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
const { getDoctorInfo } = accountStore;
const teamDisplay = computed(() => `${currentTeam.value?.name || ''}(${managedArchiveCountAllTeams.value})`);
function asArray(value) {
return Array.isArray(value) ? value : [];
}
function normalizeTeam(raw) {
if (!raw || typeof raw !== 'object') return null;
const teamId = raw.teamId || raw.id || raw._id || '';
const name = raw.name || raw.teamName || raw.team || '';
const corpId = raw.corpId || raw.corpID || '';
const userId = raw.userId || raw.userid || raw.corpUserId || '';
if (!teamId || !name) return null;
return { teamId: String(teamId), name: String(name), corpId: corpId ? String(corpId) : '', userId: userId ? String(userId) : '' };
}
function getUserId() {
const d = doctorInfo.value || {};
const a = account.value || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
}
function getCorpId() {
const t = currentTeam.value || {};
const a = account.value || {};
return String(t.corpId || a.corpId || '') || '';
}
function getTeamId() {
return String(currentTeam.value?.teamId || '') || '';
}
function sortGroupList(list) {
const { orderList, corpList, restList } = (Array.isArray(list) ? list : []).reduce(
(p, c) => {
if (typeof c?.sortOrder === 'number') p.orderList.push(c);
else if (c?.parentGroupId) p.corpList.push(c);
else p.restList.push(c);
return p;
},
{ orderList: [], corpList: [], restList: [] }
);
orderList.sort((a, b) => a.sortOrder - b.sortOrder);
return [...orderList, ...corpList, ...restList];
}
async function loadGroups() {
if (!currentTeam.value) return;
const corpId = getCorpId();
const teamId = getTeamId();
if (!corpId || !teamId) return;
const projection = { _id: 1, groupName: 1, parentGroupId: 1, sortOrder: 1 };
const res = await api('getGroups', { corpId, teamId, page: 1, pageSize: 1000, projection, countGroupMember: false });
if (!res?.success) {
teamGroups.value = [];
return;
}
const list = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.data) ? res.data.data : [];
teamGroups.value = sortGroupList(list);
// 当前 tab 如果是分组,但分组已不存在,则回退到“全部”
if (currentTabKey.value.startsWith('group:')) {
const gid = currentTabKey.value.slice('group:'.length);
if (!teamGroups.value.some((g) => String(g._id) === String(gid))) currentTabKey.value = 'all';
}
}
function getLetter(patient) {
const raw = patient?.firstLetter || patient?.nameFirstLetter || patient?.pinyinFirstLetter || patient?.letter || '';
const candidate = String(raw || '').trim();
if (candidate && /^[A-Za-z]$/.test(candidate)) return candidate.toUpperCase();
const name = String(patient?.name || '').trim();
const first = name ? name[0] : '';
if (/^[A-Za-z]$/.test(first)) return first.toUpperCase();
return '#';
}
function letterToDomId(letter) {
return `letter-${letter === '#' ? 'HASH' : letter}`;
}
function getSelectId(patient) {
return patient?._id || patient?.id || patient?.phone || patient?.mobile || '';
}
function parseCreateTime(value) {
if (!value) return null;
if (typeof value === 'number') return dayjs(value);
if (typeof value === 'string' && /^\d{10,13}$/.test(value)) {
const n = Number(value);
const ms = value.length === 10 ? n * 1000 : n;
return dayjs(ms);
}
const asString = String(value);
const d1 = dayjs(asString);
if (d1.isValid()) return d1;
const normalized = asString.replace(/\./g, '-').replace(/\//g, '-');
const d2 = dayjs(normalized);
return d2.isValid() ? d2 : null;
}
function formatPatient(raw) {
const name = raw?.name || raw?.customerName || '';
const sex = raw?.sex || raw?.gender || '';
const age = raw?.age ?? '';
const mobiles = asArray(raw?.mobiles).map(String).filter(Boolean);
const mobile = raw?.mobile ? String(raw.mobile) : (mobiles[0] || '');
const createTime = parseCreateTime(raw?.createTime);
const createTimeStr = createTime ? createTime.format('YYYY-MM-DD HH:mm') : '';
const rawTags = asArray(raw?.tags).filter((i) => typeof i === 'string');
const rawTagNames = asArray(raw?.tagNames).filter((i) => typeof i === 'string');
const tagIds = asArray(raw?.tagIds).map(String).filter(Boolean);
return {
...raw,
_id: raw?._id || raw?.id || '',
name: String(name || ''),
gender: String(sex || ''),
age,
tags: rawTags.length ? rawTags : (rawTagNames.length ? rawTagNames : tagIds),
mobiles,
mobile,
createTime: createTimeStr,
creator: raw?.creatorName || raw?.creator || '',
hospitalId: raw?.customerNumber || raw?.hospitalId || '',
record: null,
createdByDoctor: raw?.addMethod ? String(raw.addMethod) === 'manual' : Boolean(raw?.createdByDoctor),
hasBindWechat: Boolean(raw?.externalUserId || raw?.unionid || raw?.hasBindWechat),
};
}
function groupByLetter(list) {
const map = new Map();
list.forEach((item) => {
const letter = getLetter(item);
const arr = map.get(letter) || [];
arr.push(item);
map.set(letter, arr);
});
const letters = Array.from(map.keys()).sort((a, b) => {
if (a === '#') return 1;
if (b === '#') return -1;
return a.localeCompare(b);
});
return letters.map((letter) => ({ letter, data: map.get(letter) || [] }));
}
async function loadTeams() {
if (!doctorInfo.value && account.value?.openid) {
try {
await getDoctorInfo();
} catch (e) {
// ignore
}
}
const userId = getUserId();
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
if (!corpId || !userId) {
toast('缺少用户信息,请先完善个人信息');
return;
}
const res = await api('getTeamBymember', { corpId, corpUserId: userId });
if (!res?.success) {
toast(res?.message || '获取团队失败');
return;
}
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
const normalized = list.map(normalizeTeam).filter(Boolean);
teams.value = normalized;
const saved = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY);
const savedTeamId = saved?.teamId ? String(saved.teamId) : '';
currentTeam.value = normalized.find((t) => savedTeamId && t.teamId === savedTeamId) || normalized[0] || null;
if (currentTeam.value) uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, currentTeam.value);
}
async function reload(reset = true) {
if (!currentTeam.value) return;
if (loading.value) return;
const userId = getUserId();
const corpId = getCorpId();
const teamId = getTeamId();
if (!corpId || !teamId || !userId) {
toast('缺少用户/团队信息,请先完成登录与个人信息');
return;
}
if (reset) {
page.value = 1;
rawPatients.value = [];
pages.value = 0;
totalFromApi.value = 0;
}
const query = {
corpId,
userId,
teamId,
page: page.value,
pageSize: pageSize.value,
};
if (currentTab.value.kind === 'group' && currentTab.value.groupId) {
query.groupIds = [currentTab.value.groupId];
} else if (currentTab.value.kind === 'new') {
const start = dayjs().subtract(7, 'day').startOf('day').valueOf();
const end = dayjs().endOf('day').valueOf();
query.startCreateTime = start;
query.endCreateTime = end;
}
loading.value = true;
const res = await api('searchCorpCustomerWithFollowTime', query);
loading.value = false;
if (!res?.success) {
toast(res?.message || '获取患者列表失败');
return;
}
const payload =
res && typeof res === 'object'
? res.data && typeof res.data === 'object' && !Array.isArray(res.data)
? res.data
: res
: {};
const list = Array.isArray(payload.list) ? payload.list : Array.isArray(payload.data) ? payload.data : [];
const next = list.map(formatPatient);
rawPatients.value = page.value === 1 ? next : [...rawPatients.value, ...next];
pages.value = Number(payload.pages || 0) || 0;
totalFromApi.value = Number(payload.total || 0) || rawPatients.value.length;
managedArchiveCountAllTeams.value =
Number(
payload.totalAllTeams ||
payload.totalAllTeam ||
payload.totalAllTeamsCount ||
managedArchiveCountAllTeams.value ||
totalFromApi.value ||
0
) || (totalFromApi.value || 0);
}
const handlePatientClick = (patient) => {
if (isBatchMode.value) {
toggleSelect(patient);
return;
}
const id = patient._id || patient.id || patient.mobile || patient.phone || '';
uni.setStorageSync(DETAIL_STORAGE_KEY, {
_id: id,
name: patient.name,
sex: patient.gender,
age: patient.age,
mobile: patient.mobile,
mobiles: Array.isArray(patient.mobiles) ? patient.mobiles : (patient.mobile ? [patient.mobile] : []),
outpatientNo: patient.outpatientNo,
inpatientNo: patient.inpatientNo,
medicalRecordNo: patient.medicalRecordNo,
createTime: patient.createTime,
creator: patient.creator,
createdByDoctor: patient.createdByDoctor,
hasBindWechat: patient.hasBindWechat
});
uni.navigateTo({ url: `/pages/case/archive-detail?id=${encodeURIComponent(String(id))}` });
};
// Computed
const patientList = computed(() => {
const all = rawPatients.value || [];
// New Patient Filter (Last 7 days)
if (currentTab.value.kind === 'new') {
const now = dayjs();
const sevenDaysAgo = now.subtract(7, 'day').valueOf();
const flatList = all
.map((p) => {
const t = parseCreateTime(p.createTime)?.valueOf();
return t ? { ...p, _ts: t } : null;
})
.filter(Boolean)
.filter((p) => p._ts >= sevenDaysAgo)
.sort((a, b) => b._ts - a._ts);
return [{ letter: '最近新增', data: flatList }];
}
return groupByLetter(all);
});
const indexList = computed(() => {
if (currentTab.value.kind === 'new') return []; // No index bar for new patient
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').filter(l => patientList.value.some(g => g.letter === l));
});
const totalPatients = computed(() => {
let count = 0;
patientList.value.forEach(g => count += g.data.length);
return totalFromApi.value || count;
});
// Methods
const checkBatchMode = () => {
if (isBatchMode.value) {
uni.showToast({ title: '请先完成当前批量设置或点击底部“取消”按钮退出', icon: 'none' });
return true;
}
return false;
};
const scrollToLetter = (letter) => {
if (currentTab.value.kind === 'new') return;
scrollIntoId.value = letterToDomId(letter);
};
const toggleTeamPopup = () => {
if (checkBatchMode()) return;
if (!teams.value.length) {
toast('暂无可选团队');
return;
}
uni.showActionSheet({
itemList: teams.value.map((i) => i.name),
success: function (res) {
currentTeam.value = teams.value[res.tapIndex] || teams.value[0] || null;
if (currentTeam.value) uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, currentTeam.value);
currentTabKey.value = 'all';
loadGroups();
reload(true);
}
});
};
const goToSearch = () => {
if (checkBatchMode()) return;
uni.navigateTo({
url: '/pages/case/search'
});
};
const goToGroupManage = () => {
if (checkBatchMode()) return;
uni.navigateTo({
url: '/pages/case/group-manage'
});
};
const toggleBatchMode = () => {
if (isBatchMode.value) {
// Already in batch mode, do nothing or prompt?
// Prompt says "click Other operations... prompt please finish".
// Clicking "Batch" itself while in batch mode: user usually expects toggle off or nothing.
// Based on "click Cancel button to exit", I'll assume clicking existing batch button doesn't exit.
return;
}
isBatchMode.value = true;
selectedItems.value = [];
};
const handleCreate = () => {
if (checkBatchMode()) return;
// 100上限无法继续新增 -> 引导联系客服(预留入口)
if (managedArchiveCountAllTeams.value >= 100) {
uni.showModal({
title: '提示',
content: '当前管理档案数已达 100 个,无法继续新增。如需提升档案管理数,请联系客服处理。',
cancelText: '知道了',
confirmText: '添加客服',
success: (res) => {
if (res.confirm) {
openAddCustomerServiceEntry();
}
}
});
return;
}
// 未认证 + 达到10上限提示去认证
if (!isVerified.value && managedArchiveCountAllTeams.value >= 10) {
uni.showModal({
title: '提示',
content: '当前管理档案数已达上限 10 个,完成认证即可升级至 100 个。',
cancelText: '暂不认证',
confirmText: '去认证',
success: (res) => {
if (res.confirm) {
startVerifyFlow();
}
}
});
return;
}
// 未达上限:显示新增入口
uni.showActionSheet({
itemList: ['邀请患者建档', '我帮患者建档'],
success: (res) => {
if (res.tapIndex === 0) {
openInvitePatientEntry();
} else if (res.tapIndex === 1) {
openCreatePatientEntry();
}
}
});
};
// 新增流程:认证分支
const startVerifyFlow = () => {
// 有历史失败记录 -> 展示失败原因 & 重新认证
if (hasVerifyFailedHistory.value) {
uni.showModal({
title: '提示',
content: `您有历史认证未通过记录。失败原因为:\n\n${verifyFailedReason.value}`,
cancelText: '取消',
confirmText: '重新认证',
success: (res) => {
if (res.confirm) {
openVerifyEntry();
}
}
});
return;
}
// 正常去认证
openVerifyEntry();
};
// ===== 预留入口(后续对接真实页面/接口) =====
const openVerifyEntry = () => {
uni.showToast({ title: '认证功能待接入', icon: 'none' });
};
const openAddCustomerServiceEntry = () => {
uni.showToast({ title: '添加客服功能待接入', icon: 'none' });
};
const openInvitePatientEntry = () => {
uni.navigateTo({ url: '/pages/case/patient-invite' });
};
const openCreatePatientEntry = () => {
uni.navigateTo({ url: '/pages/case/patient-create' });
};
// Batch Operations
const toggleSelect = (patient) => {
if (!isBatchMode.value) return; // Should not happen if click handler is correct
const id = patient._id || patient.id || patient.phone || patient.mobile; // Prefer server id
const index = selectedItems.value.indexOf(id);
if (index > -1) {
selectedItems.value.splice(index, 1);
} else {
selectedItems.value.push(id);
}
};
const handleSelectAll = () => {
// Flatten current list
const currentList = patientList.value.flatMap(group => group.data);
if (selectedItems.value.length === currentList.length) {
selectedItems.value = []; // Unselect All
} else {
selectedItems.value = currentList.map(p => p._id || p.id || p.phone || p.mobile);
}
};
const cancelBatch = () => {
isBatchMode.value = false;
selectedItems.value = [];
};
const handleTransfer = () => {
if (selectedItems.value.length === 0) {
uni.showToast({ title: '请选择患者', icon: 'none' });
return;
}
uni.setStorageSync(BATCH_CUSTOMER_IDS_KEY, selectedItems.value.slice());
// Navigate to Transfer Page
uni.navigateTo({ url: '/pages/case/batch-transfer' });
};
const handleShare = () => {
if (selectedItems.value.length === 0) {
uni.showToast({ title: '请选择患者', icon: 'none' });
return;
}
uni.setStorageSync(BATCH_CUSTOMER_IDS_KEY, selectedItems.value.slice());
// Navigate to Share Page
uni.navigateTo({ url: '/pages/case/batch-share' });
};
function loadMore() {
if (!more.value || loading.value) return;
page.value += 1;
reload(false);
}
watch(currentTeam, (t) => {
if (!t) return;
uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, t);
});
watch(currentTabKey, () => {
if (!currentTeam.value) return;
reload(true);
});
function onTabClick(tab) {
if (checkBatchMode()) return;
if (!tab || !tab.key) return;
if (currentTabKey.value === tab.key) return;
currentTabKey.value = tab.key;
}
onLoad(async () => {
await loadTeams();
if (currentTeam.value) {
await loadGroups();
await reload(true);
}
});
onShow(async () => {
const need = uni.getStorageSync(NEED_RELOAD_STORAGE_KEY);
if (need) {
uni.removeStorageSync(NEED_RELOAD_STORAGE_KEY);
await reload(true);
// 批量操作完成后回到列表,默认退出批量态
isBatchMode.value = false;
selectedItems.value = [];
}
const needGroups = uni.getStorageSync(GROUPS_RELOAD_KEY);
if (needGroups) {
uni.removeStorageSync(GROUPS_RELOAD_KEY);
await loadGroups();
await reload(true);
} else {
await loadGroups();
}
});
</script>
<style lang="scss" scoped>
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #fff;
padding-bottom: 0; // Default
// Padding for batch footer
/* &.is-batch {
padding-bottom: 50px;
} */
// We can't use &.is-batch because scoped style and root element is tricky depending on uni-app version/style
// Instead we handle it in content-body or separate view
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
.team-selector {
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
color: #333;
.team-name {
margin-right: 5px;
}
}
.header-actions {
display: flex;
gap: 15px;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.action-text {
font-size: 10px;
color: #333;
margin-top: 2px;
}
}
}
}
.tabs-area {
display: flex;
align-items: center;
background-color: #f5f7fa;
border-bottom: 1px solid #eee;
padding-right: 15px; // Padding for the count
.tabs-scroll {
flex: 1;
white-space: nowrap;
overflow: hidden;
.tabs-container {
display: flex;
padding: 10px 15px;
.tab-item {
padding: 5px 15px;
margin-right: 10px;
font-size: 14px;
color: #666;
background-color: #fff;
border-radius: 4px;
flex-shrink: 0;
&.active {
color: #5d8aff;
background-color: #e6f0ff;
font-weight: bold;
}
}
}
}
.total-count-inline {
font-size: 12px;
color: #666;
white-space: nowrap;
flex-shrink: 0;
min-width: 50px;
text-align: right;
}
}
.content-body {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
.patient-list {
flex: 1;
height: 100%;
background-color: #f7f8fa;
}
.group-title {
padding: 5px 15px;
font-size: 14px;
color: #333;
}
.patient-card {
display: flex;
background-color: #fff;
padding: 15px;
margin-bottom: 1px; // Separator line
border-bottom: 1px solid #f0f0f0;
.checkbox-area {
display: flex;
align-items: center;
margin-right: 10px;
}
.card-content {
flex: 1;
}
.card-row-top {
display: flex;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
.patient-info {
display: flex;
align-items: flex-end;
margin-right: 10px;
.patient-name {
font-size: 18px;
font-weight: bold;
color: #333;
margin-right: 8px;
}
.patient-meta {
font-size: 12px;
color: #999;
margin-bottom: 2px;
}
}
.patient-tags {
display: flex;
gap: 5px;
.tag {
font-size: 10px;
color: #5d8aff;
border: 1px solid #5d8aff;
padding: 0 4px;
border-radius: 8px;
height: 16px;
line-height: 14px;
}
}
}
.card-row-bottom {
font-size: 14px;
.record-text {
color: #666;
}
.no-record {
color: #bdc3c7;
}
}
}
.batch-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
z-index: 99;
.left-action {
display: flex;
align-items: center;
.footer-text {
margin-left: 5px;
font-size: 14px;
color: #333;
}
}
.right-actions {
display: flex;
gap: 10px;
.footer-btn {
font-size: 14px;
padding: 0 15px;
height: 32px;
line-height: 32px;
margin: 0;
border-radius: 4px;
&.plain {
border: 1px solid #ddd;
background-color: #fff;
color: #666;
}
&.primary {
background-color: #5d8aff;
color: #fff;
border: none;
}
&::after {
border: none;
}
}
}
}
.sidebar-index {
width: 20px;
position: absolute;
right: 0;
top: 20px;
bottom: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 10;
.index-item {
font-size: 10px;
color: #555;
padding: 2px 0;
width: 20px;
text-align: center;
font-weight: 500;
}
}
</style>