melissa/paciente: aba Documentos vira pagina nativa 2-col
Antes: <DocumentsListPage embedded /> reusava o componente do Rail/Classic em modo embed — visual conflitava com o padrao Melissa, sem agrupamento por tipo, scroll inconsistente. Novo: MelissaPatientDocuments.vue (componente nativo 2-col seguindo MelissaDocumentosTemplates): - Sidebar esquerda: tipos de documento com contadores (Todos, Laudo, Receita, Exame, Termo assinado, Relatorio externo, Identidade, Convenio, Declaracao, Atestado, Recibo, Outro). Item ativo destaca primary; vazios em opacity 50%. - Main direita: header com titulo do tipo + count, DataView com cards (DocumentCard reusado), paginacao automatica >12, empty states distintos (global vs filtrado). - Header da pagina: botoes Refresh / Gerar / Upload (primary outlined no dark-friendly). - Mobile <1024px: sidebar vira drawer com botao "Tipos" no header (espelha padrao MelissaBloqueios/Templates). Reaproveita do features/documents: - useDocuments composable - DocumentCard, DocumentUploadDialog, DocumentPreviewDialog, DocumentGenerateDialog, DocumentSignatureDialog, DocumentShareDialog MelissaPaciente.vue: import DocumentsListPage -> Melissa PatientDocuments + uso na aba. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ import { useToast } from 'primevue/usetoast';
|
||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||
import { MELISSA_AGENDA_KEY } from './composables/useMelissaAgenda';
|
||||
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
|
||||
import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
|
||||
import MelissaPatientDocuments from '@/layout/melissa/MelissaPatientDocuments.vue';
|
||||
import PatientConversationsTab from '@/features/patients/prontuario/PatientConversationsTab.vue';
|
||||
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
||||
import { usePatientDetail } from '@/features/patients/composables/usePatientDetail';
|
||||
@@ -2092,16 +2092,11 @@ onBeforeUnmount(() => {
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- DocumentsListPage embedded (ja vem com upload/preview/lista) -->
|
||||
<section class="mpa-w mpa-embed">
|
||||
<div class="mpa-w__body mpa-embed__body">
|
||||
<DocumentsListPage
|
||||
:patient-id="patientId"
|
||||
:patient-name="nomeCompleto || ''"
|
||||
embedded
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Documentos nativos Melissa (2-col com tipos na sidebar) -->
|
||||
<MelissaPatientDocuments
|
||||
:patient-id="patientId"
|
||||
:patient-name="nomeCompleto || ''"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,699 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI — MelissaPatientDocuments
|
||||
|--------------------------------------------------------------------------
|
||||
| Página nativa Melissa pra aba "Documentos" do prontuário (substitui o
|
||||
| embed do DocumentsListPage).
|
||||
|
|
||||
| Layout 2-col (espelha MelissaDocumentosTemplates):
|
||||
| - COL 1 (esquerda): tipos de documento como sidebar com contadores
|
||||
| - COL 2 (direita): DataView dos documentos do tipo selecionado,
|
||||
| com upload/preview/edit/sign/share/delete via dialogs reaproveitados
|
||||
| - Mobile (<1024px): sidebar vira drawer (botão "Tipos" no header)
|
||||
|
|
||||
| Reaproveita do feature/documents:
|
||||
| - useDocuments composable (fetch + CRUD + URLs)
|
||||
| - DocumentCard pra item visual
|
||||
| - DocumentUploadDialog / PreviewDialog / GenerateDialog /
|
||||
| SignatureDialog / ShareDialog
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import Menu from 'primevue/menu';
|
||||
import DataView from 'primevue/dataview';
|
||||
|
||||
import { useDocuments } from '@/features/documents/composables/useDocuments';
|
||||
import DocumentCard from '@/features/documents/components/DocumentCard.vue';
|
||||
import DocumentUploadDialog from '@/features/documents/components/DocumentUploadDialog.vue';
|
||||
import DocumentPreviewDialog from '@/features/documents/components/DocumentPreviewDialog.vue';
|
||||
import DocumentGenerateDialog from '@/features/documents/components/DocumentGenerateDialog.vue';
|
||||
import DocumentSignatureDialog from '@/features/documents/components/DocumentSignatureDialog.vue';
|
||||
import DocumentShareDialog from '@/features/documents/components/DocumentShareDialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
patientId: { type: String, default: null },
|
||||
patientName: { type: String, default: '' }
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
|
||||
// ── useDocuments composable ───────────────────────────────
|
||||
const {
|
||||
documents, loading, error, filters, usedTags, stats,
|
||||
TIPOS_DOCUMENTO,
|
||||
fetchDocuments, upload, update, remove,
|
||||
download, getPreviewUrl, fetchUsedTags
|
||||
} = useDocuments(() => props.patientId);
|
||||
|
||||
// ── Dialogs ─────────────────────────────────────────────────
|
||||
const uploadDlg = ref(false);
|
||||
const previewDlg = ref(false);
|
||||
const generateDlg = ref(false);
|
||||
const signatureDlg = ref(false);
|
||||
const shareDlg = ref(false);
|
||||
const selectedDoc = ref(null);
|
||||
const previewUrl = ref('');
|
||||
|
||||
// ── Tipo selecionado (filtro pela sidebar) ────────────────
|
||||
// null = todos os tipos
|
||||
const selectedTipo = ref(null);
|
||||
|
||||
const docsByTipo = computed(() => {
|
||||
const groups = {};
|
||||
for (const d of documents.value) {
|
||||
const tipo = d.tipo_documento || 'outro';
|
||||
if (!groups[tipo]) groups[tipo] = [];
|
||||
groups[tipo].push(d);
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
|
||||
const tipoCounts = computed(() => {
|
||||
const counts = {};
|
||||
TIPOS_DOCUMENTO.forEach(t => { counts[t.value] = (docsByTipo.value[t.value] || []).length; });
|
||||
return counts;
|
||||
});
|
||||
|
||||
const filteredDocs = computed(() => {
|
||||
if (!selectedTipo.value) return documents.value;
|
||||
return docsByTipo.value[selectedTipo.value] || [];
|
||||
});
|
||||
|
||||
function tipoLabel(tipo) {
|
||||
return TIPOS_DOCUMENTO.find(t => t.value === tipo)?.label || tipo;
|
||||
}
|
||||
|
||||
// ── Mobile drawer ─────────────────────────────────────────
|
||||
const drawerOpen = ref(false);
|
||||
const isMobile = ref(false);
|
||||
let _mqMobile = null;
|
||||
function _onMqMobileChange(e) {
|
||||
isMobile.value = e.matches;
|
||||
if (!e.matches) drawerOpen.value = false;
|
||||
}
|
||||
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
|
||||
function fecharDrawer() { drawerOpen.value = false; }
|
||||
function selectTipo(tipo) {
|
||||
selectedTipo.value = tipo;
|
||||
if (isMobile.value) drawerOpen.value = false;
|
||||
}
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────
|
||||
async function onUploaded({ file, meta }) {
|
||||
try {
|
||||
await upload(file, props.patientId, meta);
|
||||
toast.add({ severity: 'success', summary: 'Enviado', detail: file.name, life: 3000 });
|
||||
fetchUsedTags();
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro no upload', detail: e?.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function onPreview(doc) {
|
||||
selectedDoc.value = doc;
|
||||
try { previewUrl.value = await getPreviewUrl(doc); }
|
||||
catch { previewUrl.value = ''; }
|
||||
previewDlg.value = true;
|
||||
}
|
||||
|
||||
function onDownload(doc) { download(doc); }
|
||||
|
||||
function onEdit(doc) {
|
||||
selectedDoc.value = doc;
|
||||
// Reusa preview dialog em modo "ver" — edit completo só via DocumentsListPage
|
||||
onPreview(doc);
|
||||
}
|
||||
|
||||
function onDelete(doc) {
|
||||
confirm.require({
|
||||
header: 'Excluir documento',
|
||||
message: `Excluir "${doc.nome_original}"? Esta ação pode ser revertida no Lixo.`,
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptLabel: 'Excluir',
|
||||
rejectLabel: 'Cancelar',
|
||||
acceptSeverity: 'danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
await remove(doc.id);
|
||||
toast.add({ severity: 'success', summary: 'Excluído', detail: doc.nome_original, life: 3000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro ao excluir', detail: e?.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onShare(doc) {
|
||||
selectedDoc.value = doc;
|
||||
shareDlg.value = true;
|
||||
}
|
||||
function onSign(doc) {
|
||||
selectedDoc.value = doc;
|
||||
signatureDlg.value = true;
|
||||
}
|
||||
function onGenerated() {
|
||||
fetchDocuments();
|
||||
fetchUsedTags();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||
_mqMobile = window.matchMedia('(max-width: 1023px)');
|
||||
isMobile.value = _mqMobile.matches;
|
||||
try { _mqMobile.addEventListener('change', _onMqMobileChange); }
|
||||
catch { _mqMobile.addListener(_onMqMobileChange); }
|
||||
}
|
||||
await Promise.all([fetchDocuments(), fetchUsedTags()]);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (_mqMobile) {
|
||||
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
|
||||
catch { _mqMobile.removeListener(_onMqMobileChange); }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Mobile drawer (tipos) -->
|
||||
<Transition name="mpd-drawer-fade">
|
||||
<div
|
||||
v-show="isMobile && drawerOpen"
|
||||
class="mpd-mobile-drawer"
|
||||
:class="{ 'is-open': drawerOpen }"
|
||||
>
|
||||
<div id="mpd-mobile-drawer-target" class="mpd-mobile-drawer__scroll" />
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="mpd-drawer-fade">
|
||||
<div
|
||||
v-show="isMobile && drawerOpen"
|
||||
class="mpd-mobile-drawer__backdrop"
|
||||
@click="fecharDrawer"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<section class="mpd-page">
|
||||
<!-- ── HEADER ─────────────────────────────────────── -->
|
||||
<header class="mpd-page__head">
|
||||
<button
|
||||
class="mpd-menu-btn mpd-menu-btn--mobile-only"
|
||||
v-tooltip.bottom="'Tipos de documento'"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<i class="pi pi-bars" />
|
||||
<span>Tipos</span>
|
||||
</button>
|
||||
<div class="mpd-page__title">
|
||||
<i class="pi pi-file mpd-page__title-icon" />
|
||||
<span>Documentos</span>
|
||||
<span class="mpd-page__count">{{ documents.length }}</span>
|
||||
</div>
|
||||
<div class="mpd-page__actions">
|
||||
<button
|
||||
class="mpd-act-btn"
|
||||
v-tooltip.bottom="'Atualizar'"
|
||||
:disabled="loading"
|
||||
@click="fetchDocuments"
|
||||
>
|
||||
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
|
||||
</button>
|
||||
<button
|
||||
class="mpd-act-btn"
|
||||
v-tooltip.bottom="'Gerar a partir de template'"
|
||||
:disabled="!patientId"
|
||||
@click="generateDlg = true"
|
||||
>
|
||||
<i class="pi pi-file-pdf" />
|
||||
<span>Gerar</span>
|
||||
</button>
|
||||
<button
|
||||
class="mpd-act-btn mpd-act-btn--primary"
|
||||
v-tooltip.bottom="'Enviar arquivo'"
|
||||
:disabled="!patientId"
|
||||
@click="uploadDlg = true"
|
||||
>
|
||||
<i class="pi pi-upload" />
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── BODY 2-col ─────────────────────────────────── -->
|
||||
<div class="mpd-body">
|
||||
<!-- COL 1 — Sidebar de tipos (teleporta no mobile) -->
|
||||
<Teleport to="#mpd-mobile-drawer-target" :disabled="!isMobile">
|
||||
<aside class="mpd-side">
|
||||
<div class="mpd-side__head">
|
||||
<i class="pi pi-folder" />
|
||||
<span>Tipos</span>
|
||||
</div>
|
||||
<ul class="mpd-side__list">
|
||||
<li
|
||||
class="mpd-side__item"
|
||||
:class="{ 'is-active': selectedTipo === null }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectTipo(null)"
|
||||
@keydown.enter.prevent="selectTipo(null)"
|
||||
>
|
||||
<span class="mpd-side__item-name">Todos</span>
|
||||
<span class="mpd-side__item-count">{{ documents.length }}</span>
|
||||
</li>
|
||||
<li
|
||||
v-for="t in TIPOS_DOCUMENTO"
|
||||
:key="t.value"
|
||||
class="mpd-side__item"
|
||||
:class="{ 'is-active': selectedTipo === t.value, 'is-empty': tipoCounts[t.value] === 0 }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectTipo(t.value)"
|
||||
@keydown.enter.prevent="selectTipo(t.value)"
|
||||
>
|
||||
<span class="mpd-side__item-name">{{ t.label }}</span>
|
||||
<span class="mpd-side__item-count">{{ tipoCounts[t.value] || 0 }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</Teleport>
|
||||
|
||||
<!-- COL 2 — Main: lista de documentos -->
|
||||
<main class="mpd-main">
|
||||
<header class="mpd-main__head">
|
||||
<div class="mpd-main__title-row">
|
||||
<div class="mpd-main__title">
|
||||
<i class="pi pi-file" />
|
||||
<span>{{ selectedTipo ? tipoLabel(selectedTipo) : 'Todos os documentos' }}</span>
|
||||
</div>
|
||||
<span class="mpd-page__count">{{ filteredDocs.length }}</span>
|
||||
</div>
|
||||
<p class="mpd-main__subtitle">
|
||||
<template v-if="selectedTipo">
|
||||
Documentos do tipo <strong>{{ tipoLabel(selectedTipo) }}</strong> deste paciente.
|
||||
</template>
|
||||
<template v-else>
|
||||
Todos os documentos clínicos vinculados a este paciente.
|
||||
</template>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading && !documents.length" class="mpd-loading">
|
||||
<i class="pi pi-spin pi-spinner" />
|
||||
<span>Carregando documentos…</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty global -->
|
||||
<div v-else-if="!documents.length" class="mpd-empty">
|
||||
<i class="pi pi-folder-open mpd-empty__icon" />
|
||||
<div class="mpd-empty__title">Nenhum documento ainda</div>
|
||||
<div class="mpd-empty__hint">
|
||||
Faça upload de um arquivo ou gere um documento a partir de template.
|
||||
</div>
|
||||
<div class="mpd-empty__actions">
|
||||
<button class="mpd-act-btn" @click="generateDlg = true">
|
||||
<i class="pi pi-file-pdf" /><span>Gerar template</span>
|
||||
</button>
|
||||
<button class="mpd-act-btn mpd-act-btn--primary" @click="uploadDlg = true">
|
||||
<i class="pi pi-upload" /><span>Upload</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty (filtrado) -->
|
||||
<div v-else-if="!filteredDocs.length" class="mpd-empty">
|
||||
<i class="pi pi-filter mpd-empty__icon" />
|
||||
<div class="mpd-empty__title">Nenhum {{ tipoLabel(selectedTipo).toLowerCase() }} ainda</div>
|
||||
<div class="mpd-empty__hint">
|
||||
Outros tipos têm documentos —
|
||||
<button class="mpd-link" @click="selectTipo(null)">ver todos</button>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DataView -->
|
||||
<DataView
|
||||
v-else
|
||||
:value="filteredDocs"
|
||||
layout="grid"
|
||||
:paginator="filteredDocs.length > 12"
|
||||
:rows="12"
|
||||
class="mpd-dataview"
|
||||
>
|
||||
<template #grid="slotProps">
|
||||
<div class="mpd-grid">
|
||||
<DocumentCard
|
||||
v-for="doc in slotProps.items"
|
||||
:key="doc.id"
|
||||
:doc="doc"
|
||||
@preview="onPreview"
|
||||
@download="onDownload"
|
||||
@edit="onEdit"
|
||||
@delete="onDelete"
|
||||
@share="onShare"
|
||||
@sign="onSign"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</DataView>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- ── Dialogs reaproveitados ─────────────────────── -->
|
||||
<DocumentUploadDialog
|
||||
v-model:visible="uploadDlg"
|
||||
:patient-id="patientId"
|
||||
@uploaded="onUploaded"
|
||||
/>
|
||||
<DocumentPreviewDialog
|
||||
v-model:visible="previewDlg"
|
||||
:doc="selectedDoc"
|
||||
:preview-url="previewUrl"
|
||||
@updated="fetchDocuments"
|
||||
/>
|
||||
<DocumentGenerateDialog
|
||||
v-if="patientId"
|
||||
v-model:visible="generateDlg"
|
||||
:patient-id="patientId"
|
||||
:patient-name="patientName"
|
||||
@generated="onGenerated"
|
||||
/>
|
||||
<DocumentSignatureDialog
|
||||
:visible="signatureDlg"
|
||||
@update:visible="signatureDlg = $event"
|
||||
:doc="selectedDoc"
|
||||
/>
|
||||
<DocumentShareDialog
|
||||
v-model:visible="shareDlg"
|
||||
:doc="selectedDoc"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ═══════ Page chrome ═══════ */
|
||||
.mpd-page {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--m-bg-medium);
|
||||
border: 1px solid var(--m-border);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
color: var(--m-text);
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.mpd-page__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--m-border);
|
||||
flex-shrink: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
.mpd-page__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.mpd-page__title-icon { color: var(--p-primary-color); font-size: 1rem; }
|
||||
.mpd-page__count {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--m-accent);
|
||||
background: var(--m-accent-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--m-accent) 35%, transparent);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.mpd-page__actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
||||
|
||||
.mpd-act-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 9px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
transition: background-color 140ms ease;
|
||||
background: var(--m-bg-soft);
|
||||
border: 1px solid var(--m-border);
|
||||
color: var(--m-text);
|
||||
}
|
||||
.mpd-act-btn:hover:not(:disabled) { background: var(--m-bg-soft-hover); }
|
||||
.mpd-act-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.mpd-act-btn--primary {
|
||||
background: transparent;
|
||||
border-color: var(--p-primary-color);
|
||||
color: var(--p-primary-color);
|
||||
}
|
||||
.mpd-act-btn--primary:hover:not(:disabled) { background: color-mix(in srgb, var(--p-primary-color) 10%, transparent); }
|
||||
.mpd-act-btn > i { font-size: 0.78rem; }
|
||||
|
||||
.mpd-menu-btn {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--m-border);
|
||||
background: var(--m-bg-soft);
|
||||
color: var(--m-text);
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mpd-menu-btn:hover { background: var(--m-bg-soft-hover); }
|
||||
|
||||
/* ═══════ Body 2-col ═══════ */
|
||||
.mpd-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 260px) 1fr;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* COL 1 — Sidebar */
|
||||
.mpd-side {
|
||||
background: var(--m-bg-soft);
|
||||
border: 1px solid var(--m-border);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mpd-side__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--m-border);
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mpd-side__head > i { color: var(--m-text-muted); font-size: 0.9rem; }
|
||||
.mpd-side__list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
list-style: none;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.mpd-side__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
transition: background-color 120ms ease, color 120ms ease;
|
||||
}
|
||||
.mpd-side__item:hover,
|
||||
.mpd-side__item:focus-visible {
|
||||
background: var(--m-bg-soft-hover);
|
||||
outline: none;
|
||||
}
|
||||
.mpd-side__item.is-active {
|
||||
background: color-mix(in srgb, var(--p-primary-color) 12%, var(--m-bg-medium));
|
||||
color: var(--p-primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
.mpd-side__item.is-empty { opacity: 0.5; }
|
||||
.mpd-side__item-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mpd-side__item-count {
|
||||
font-size: 0.7rem;
|
||||
color: var(--m-text-muted);
|
||||
background: var(--m-bg-medium);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mpd-side__item.is-active .mpd-side__item-count {
|
||||
color: var(--p-primary-color);
|
||||
background: color-mix(in srgb, var(--p-primary-color) 18%, transparent);
|
||||
}
|
||||
|
||||
/* COL 2 — Main */
|
||||
.mpd-main {
|
||||
background: var(--m-bg-soft);
|
||||
border: 1px solid var(--m-border);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mpd-main__head {
|
||||
padding: 12px 14px 8px;
|
||||
border-bottom: 1px solid var(--m-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mpd-main__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.mpd-main__title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.mpd-main__title > i { color: var(--p-primary-color); font-size: 0.9rem; }
|
||||
.mpd-main__subtitle {
|
||||
margin: 6px 0 0;
|
||||
font-size: 0.74rem;
|
||||
color: var(--m-text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.mpd-loading,
|
||||
.mpd-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 28px;
|
||||
text-align: center;
|
||||
color: var(--m-text-muted);
|
||||
gap: 8px;
|
||||
}
|
||||
.mpd-loading > i { font-size: 1.4rem; color: var(--p-primary-color); }
|
||||
.mpd-empty__icon { font-size: 2.4rem; color: var(--m-text-faint); margin-bottom: 6px; }
|
||||
.mpd-empty__title { font-size: 0.95rem; font-weight: 600; color: var(--m-text); }
|
||||
.mpd-empty__hint { font-size: 0.82rem; max-width: 360px; line-height: 1.5; }
|
||||
.mpd-empty__actions { display: flex; gap: 8px; margin-top: 10px; }
|
||||
.mpd-link {
|
||||
background: none; border: none; color: var(--p-primary-color);
|
||||
cursor: pointer; padding: 0; font: inherit; text-decoration: underline;
|
||||
}
|
||||
|
||||
.mpd-dataview {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
.mpd-dataview :deep(.p-dataview-content) {
|
||||
flex: 1; min-height: 0; overflow-y: auto;
|
||||
background: transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.mpd-dataview :deep(.p-dataview-paginator-bottom) {
|
||||
flex-shrink: 0; background: transparent;
|
||||
border-top: 1px solid var(--m-border);
|
||||
}
|
||||
.mpd-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* ═══════ Mobile drawer ═══════ */
|
||||
.mpd-mobile-drawer {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
height: 100vh; height: 100dvh;
|
||||
width: min(320px, 86vw);
|
||||
z-index: 80;
|
||||
background: var(--m-bg-medium);
|
||||
backdrop-filter: blur(28px) saturate(160%);
|
||||
-webkit-backdrop-filter: blur(28px) saturate(160%);
|
||||
border-right: 1px solid var(--m-border);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: var(--m-text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mpd-mobile-drawer.is-open { transform: translateX(0); }
|
||||
.mpd-mobile-drawer:not(.is-open) { pointer-events: none; }
|
||||
.mpd-mobile-drawer__scroll {
|
||||
flex: 1; min-height: 0; overflow-y: auto;
|
||||
padding: 12px; display: flex; flex-direction: column;
|
||||
}
|
||||
.mpd-mobile-drawer__scroll .mpd-side {
|
||||
width: 100%; height: 100%; border: 1px solid var(--m-border);
|
||||
}
|
||||
.mpd-mobile-drawer__backdrop {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 79;
|
||||
}
|
||||
.mpd-drawer-fade-enter-active,
|
||||
.mpd-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
||||
.mpd-drawer-fade-enter-from,
|
||||
.mpd-drawer-fade-leave-to { opacity: 0; }
|
||||
|
||||
/* ═══════ Mobile (<1024px) ═══════ */
|
||||
@media (max-width: 1023px) {
|
||||
.mpd-body {
|
||||
grid-template-columns: 1fr;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mpd-body > .mpd-side { display: none; }
|
||||
.mpd-menu-btn--mobile-only { display: inline-flex; }
|
||||
.mpd-page__title > span:first-of-type { display: none; }
|
||||
.mpd-page__title-icon { display: none; }
|
||||
.mpd-act-btn span { display: none; }
|
||||
.mpd-act-btn { width: 32px; padding: 0; justify-content: center; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user