ykt-wxapp/pages/case/case.vue

929 lines
25 KiB
Vue
Raw Normal View History

2026-01-20 10:49:33 +08:00
<template>
2026-01-20 16:24:43 +08:00
<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, index) in tabs"
:key="index"
class="tab-item"
:class="{ active: currentTab === index }"
@click="currentTab = index"
>
{{ tab }}
</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"
2026-01-20 16:24:43 +08:00
>
<view v-for="group in patientList" :key="group.letter" :id="letterToDomId(group.letter)">
2026-01-20 16:24:43 +08:00
<view class="group-title">{{ group.letter }}</view>
<view v-for="(patient, pIndex) in group.data" :key="pIndex" class="patient-card" @click="handlePatientClick(patient)">
2026-01-20 16:24:43 +08:00
<!-- Checkbox for Batch Mode -->
<view v-if="isBatchMode" class="checkbox-area">
<uni-icons
:type="selectedItems.includes(getSelectId(patient)) ? 'checkbox-filled' : 'checkbox'"
2026-01-20 16:24:43 +08:00
size="24"
:color="selectedItems.includes(getSelectId(patient)) ? '#007aff' : '#ccc'"
2026-01-20 16:24:43 +08:00
></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="currentTab === 1"> <!-- 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>
2026-01-20 10:49:33 +08:00
</template>
2026-01-20 16:24:43 +08:00
<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';
2026-01-20 16:24:43 +08:00
// State
const teams = ref([]);
const currentTeam = ref(null);
2026-01-20 16:24:43 +08:00
const currentTab = ref(0);
const scrollIntoId = ref('');
const tabs = ['全部', '新患者', '糖尿病', '高血压', '冠心病', '慢阻肺'];
const isBatchMode = ref(false);
const selectedItems = ref([]); // Stores patient phone or unique ID
// 新增流程所需状态(后续接接口替换)
const managedArchiveCountAllTeams = ref(0); // 在管档案数(所有团队)
2026-01-20 16:24:43 +08:00
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 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 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 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;
2026-01-20 16:24:43 +08:00
}
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;
}
loading.value = true;
const res = await api('searchCorpCustomerWithFollowTime', {
corpId,
userId,
teamId,
page: page.value,
pageSize: pageSize.value,
});
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);
}
2026-01-20 16:24:43 +08:00
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))}` });
};
2026-01-20 16:24:43 +08:00
// Computed
const patientList = computed(() => {
const all = rawPatients.value || [];
2026-01-20 16:24:43 +08:00
// New Patient Filter (Last 7 days)
if (currentTab.value === 1) {
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);
2026-01-20 16:24:43 +08:00
return [{ letter: '最近新增', data: flatList }];
}
// Tab Filtering (Mock logic for other tabs)
let filtered = all;
2026-01-20 16:24:43 +08:00
if (currentTab.value > 1) {
const tabName = tabs[currentTab.value];
filtered = filtered.filter((p) => Array.isArray(p.tags) && p.tags.includes(tabName));
2026-01-20 16:24:43 +08:00
}
return groupByLetter(filtered);
2026-01-20 16:24:43 +08:00
});
const indexList = computed(() => {
if (currentTab.value === 1) 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);
if (currentTab.value === 0 && totalFromApi.value) return totalFromApi.value;
2026-01-20 16:24:43 +08:00
return count;
});
// Methods
const checkBatchMode = () => {
if (isBatchMode.value) {
uni.showToast({ title: '请先完成当前批量设置或点击底部“取消”按钮退出', icon: 'none' });
return true;
}
return false;
};
const scrollToLetter = (letter) => {
if (currentTab.value === 1) return;
scrollIntoId.value = letterToDomId(letter);
2026-01-20 16:24:43 +08:00
};
const toggleTeamPopup = () => {
if (checkBatchMode()) return;
if (!teams.value.length) {
toast('暂无可选团队');
return;
}
2026-01-20 16:24:43 +08:00
uni.showActionSheet({
itemList: teams.value.map((i) => i.name),
2026-01-20 16:24:43 +08:00
success: function (res) {
currentTeam.value = teams.value[res.tapIndex] || teams.value[0] || null;
if (currentTeam.value) uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, currentTeam.value);
reload(true);
2026-01-20 16:24:43 +08:00
}
});
};
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
2026-01-20 16:24:43 +08:00
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);
2026-01-20 16:24:43 +08:00
}
};
const cancelBatch = () => {
isBatchMode.value = false;
selectedItems.value = [];
};
const handleTransfer = () => {
if (selectedItems.value.length === 0) {
uni.showToast({ title: '请选择患者', icon: 'none' });
return;
}
// Navigate to Transfer Page
uni.navigateTo({ url: '/pages/case/batch-transfer' });
};
const handleShare = () => {
if (selectedItems.value.length === 0) {
uni.showToast({ title: '请选择患者', icon: 'none' });
return;
}
// Navigate to Share Page
uni.navigateTo({ url: '/pages/case/batch-share' });
};
function loadMore() {
if (currentTab.value === 1) return;
if (!more.value || loading.value) return;
page.value += 1;
reload(false);
}
watch(currentTeam, (t) => {
if (!t) return;
uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, t);
});
onLoad(async () => {
await loadTeams();
if (currentTeam.value) await reload(true);
});
onShow(async () => {
const need = uni.getStorageSync(NEED_RELOAD_STORAGE_KEY);
if (need) {
uni.removeStorageSync(NEED_RELOAD_STORAGE_KEY);
await reload(true);
}
});
2026-01-20 10:49:33 +08:00
</script>
2026-01-20 16:24:43 +08:00
<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;
2026-01-20 10:49:33 +08:00
2026-01-20 16:24:43 +08:00
.index-item {
font-size: 10px;
color: #555;
padding: 2px 0;
width: 20px;
text-align: center;
font-weight: 500;
}
}
</style>