Files
ctms/frontend/src/views/documents/DocumentList.vue
T

980 lines
29 KiB
Vue

<template>
<div class="page ctms-page-shell page--flush" :class="{ 'document-page--desktop': isDesktop }" @click="closeDocumentContextMenu">
<div class="main-content-flat unified-shell">
<div class="filter-container unified-action-bar bar--flush">
<el-form :inline="true" :model="filters" class="filter-form">
<div class="filter-item-form">
<el-select v-model="filters.scope_type" :placeholder="TEXT.modules.fileVersionManagement.filters.scope" clearable class="filter-select-comp">
<el-option v-for="item in scopeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="filter-item-form">
<el-select v-model="filters.site_id" :placeholder="TEXT.modules.fileVersionManagement.filters.site" clearable filterable class="filter-select-comp">
<el-option v-for="item in siteOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="filter-item-form">
<el-select v-model="filters.doc_type" :placeholder="TEXT.modules.fileVersionManagement.filters.docType" clearable class="filter-select-comp">
<el-option v-for="item in docTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="filter-item-form">
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
</div>
<div class="filter-spacer"></div>
<el-button v-if="canCreate" type="primary" @click="openCreate" class="header-action-btn">
<el-icon class="el-icon--left"><Plus /></el-icon>
{{ TEXT.modules.fileVersionManagement.newDocument }}
</el-button>
</el-form>
</div>
<section class="unified-section document-table-section section--flush-x section--flush-top section--flush-bottom">
<div class="document-workbench" :class="{ 'is-desktop': isDesktop }">
<div class="document-table-pane" :tabindex="isDesktop ? 0 : undefined" @keydown.enter.prevent="openSelectedDocument">
<el-table
:data="sortedItems"
v-loading="loading"
@row-click="handleDocumentRowClick"
@row-dblclick="openDocumentDetail"
@row-contextmenu="openDocumentContextMenu"
:row-class-name="documentRowClass"
class="ctms-table"
highlight-current-row
table-layout="fixed"
>
<el-table-column prop="title" :label="TEXT.modules.fileVersionManagement.columns.title" show-overflow-tooltip>
<template #default="{ row }">
<span class="font-semibold">{{ row.title }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.scope">
<template #default="{ row }">
<el-tag
effect="plain"
:type="row.scope_type === 'SITE' ? 'warning' : 'success'"
>
{{ displayEnum(TEXT.enums.scopeType, row.scope_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.site" show-overflow-tooltip>
<template #default="{ row }">
<span>{{ displaySite(row.site_id) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.docType">
<template #default="{ row }">
<el-tag effect="plain" type="info">{{ displayText(row.doc_type, TEXT.enums.documentType) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.currentVersion">
<template #default="{ row }">
<span v-if="row.current_effective_version">{{ row.current_effective_version.version_no }}</span>
<span v-else class="text-secondary">-</span>
</template>
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.modules.fileVersionManagement.columns.updatedAt" show-overflow-tooltip>
<template #default="{ row }">
<span class="text-secondary datetime-stack">
<span>{{ splitDateTime(row.updated_at).date }}</span>
<span class="datetime-stack__time">{{ splitDateTime(row.updated_at).time }}</span>
</span>
</template>
</el-table-column>
<el-table-column v-if="!isDesktop && (canUpdate || canDelete)" :label="TEXT.modules.fileVersionManagement.columns.actions" width="130" fixed="right">
<template #default="{ row }">
<div class="cell-actions">
<el-button v-if="canUpdate" link type="primary" size="small" :disabled="isInactiveSite(row.site_id)" @click.stop="openEdit(row)">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button v-if="canDelete" link type="danger" size="small" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
{{ TEXT.common.actions.delete }}
</el-button>
</div>
</template>
</el-table-column>
<template #empty>
<el-empty :description="TEXT.modules.fileVersionManagement.emptyDescription" />
</template>
</el-table>
</div>
<aside v-if="isDesktop" class="document-preview-pane">
<template v-if="selectedDocument">
<div class="preview-head">
<div>
<div class="preview-kicker">当前文档</div>
<div class="preview-title">{{ selectedDocument.title || TEXT.common.fallback }}</div>
</div>
<el-button size="small" type="primary" @click="openSelectedDocument">打开详情</el-button>
</div>
<dl class="preview-list">
<dt>{{ TEXT.modules.fileVersionManagement.columns.scope }}</dt>
<dd>{{ displayEnum(TEXT.enums.scopeType, selectedDocument.scope_type) }}</dd>
<dt>{{ TEXT.modules.fileVersionManagement.columns.site }}</dt>
<dd>{{ displaySite(selectedDocument.site_id) }}</dd>
<dt>{{ TEXT.modules.fileVersionManagement.columns.docType }}</dt>
<dd>{{ displayText(selectedDocument.doc_type, TEXT.enums.documentType) }}</dd>
<dt>{{ TEXT.modules.fileVersionManagement.columns.currentVersion }}</dt>
<dd>{{ selectedDocument.current_effective_version?.version_no || TEXT.common.fallback }}</dd>
<dt>{{ TEXT.modules.fileVersionManagement.columns.updatedAt }}</dt>
<dd>{{ displayDateTime(selectedDocument.updated_at) }}</dd>
</dl>
<div class="preview-actions">
<el-button size="small" @click="openSelectedDocument">打开详情</el-button>
<el-button v-if="canUpdate" size="small" :disabled="isInactiveSite(selectedDocument.site_id)" @click="openSelectedDocumentEditor">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button
v-if="canDelete"
size="small"
type="danger"
plain
:disabled="isInactiveSite(selectedDocument.site_id)"
@click="deleteSelectedDocument"
>
{{ TEXT.common.actions.delete }}
</el-button>
</div>
</template>
<div v-else class="preview-empty">
<span>选择一行查看文档摘要</span>
</div>
</aside>
</div>
</section>
</div>
<div
v-if="isDesktop && documentContextMenu.visible && selectedDocument"
class="document-context-menu"
:style="{ left: `${documentContextMenu.x}px`, top: `${documentContextMenu.y}px` }"
@click.stop
>
<button type="button" @click="openSelectedDocument">打开详情</button>
<button
v-if="canUpdate"
type="button"
:disabled="isInactiveSite(selectedDocument.site_id)"
@click="openSelectedDocumentEditor"
>
{{ TEXT.common.actions.edit }}
</button>
<button
v-if="canDelete"
type="button"
class="danger"
:disabled="isInactiveSite(selectedDocument.site_id)"
@click="deleteSelectedDocument"
>
{{ TEXT.common.actions.delete }}
</button>
</div>
<el-drawer
v-if="editorVisible"
v-model="editorVisible"
direction="rtl"
size="620px"
:close-on-click-modal="true"
:before-close="editorDirtyGuard.beforeClose"
:show-close="false"
destroy-on-close
class="document-editor-drawer"
>
<template #header>
<div class="editor-header">
<div class="editor-title">{{ editingDocumentId ? TEXT.common.actions.edit : TEXT.modules.fileVersionManagement.dialog.createTitle }}</div>
</div>
</template>
<el-form ref="editorFormRef" :model="editorForm" :rules="editorRules" label-position="top" class="document-form">
<div class="form-group">
<div class="form-group-title">
<span class="group-dot group-dot-basic"></span>
{{ TEXT.common.labels.basicInfo }}
</div>
<el-row :gutter="16">
<el-col v-if="!editingDocumentId" :span="12">
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.docNo" prop="doc_no">
<el-input v-model="editorForm.doc_no" placeholder="e.g. SOP-001" />
</el-form-item>
</el-col>
<el-col :span="editingDocumentId ? 24 : 12">
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.title" prop="title">
<el-input v-model="editorForm.title" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.scope" prop="scope_type">
<el-select v-model="editorForm.scope_type" class="full-width">
<el-option v-for="item in scopeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.docType" prop="doc_type">
<el-select v-model="editorForm.doc_type" class="full-width">
<el-option v-for="item in docTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item v-if="editorForm.scope_type === 'SITE'" :label="TEXT.modules.fileVersionManagement.fields.site" prop="site_id">
<el-select v-model="editorForm.site_id" filterable class="full-width">
<el-option v-for="item in siteOptions" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
</el-select>
</el-form-item>
</div>
</el-form>
<template #footer>
<div class="drawer-footer">
<el-button @click="editorVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="saving" @click="submitEditor">{{ TEXT.common.actions.confirm }}</el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import { fetchDocuments, createDocument, deleteDocument, updateDocument } from "../../api/documents";
import { fetchSites } from "../../api/sites";
import { useStudyStore } from "../../store/study";
import { useAuthStore } from "../../store/auth";
import { usePermission } from "../../utils/permission";
import { useDrawerDirtyGuard } from "../../utils/drawerDirtyGuard";
import type { DocumentSummary } from "../../types/documents";
import type { Site } from "../../types/api";
import { displayDateTime, displayEnum, displayText } from "../../utils/display";
import { TEXT } from "../../locales";
import { onDesktopRefreshCurrentView } from "../../composables/useDesktopRefresh";
import { isTauriRuntime } from "../../runtime";
const route = useRoute();
const router = useRouter();
const study = useStudyStore();
const auth = useAuthStore();
const { can } = usePermission();
const loading = ref(false);
const items = ref<DocumentSummary[]>([]);
const sites = ref<Site[]>([]);
const editorVisible = ref(false);
const saving = ref(false);
const editingDocumentId = ref("");
const editorFormRef = ref<FormInstance>();
const isDesktop = isTauriRuntime();
const selectedDocumentId = ref("");
const documentContextMenu = ref({ visible: false, x: 0, y: 0 });
let desktopRefreshCleanup: (() => void) | undefined;
const trialId = computed(() => (route.params.trialId as string) || "");
const filters = reactive({
scope_type: "",
site_id: "",
doc_type: "",
});
const docTypeOptions = Object.keys(TEXT.enums.documentType).map((value) => ({
value,
label: TEXT.enums.documentType[value as keyof typeof TEXT.enums.documentType],
}));
const scopeOptions = [
{ value: "GLOBAL", label: TEXT.enums.scopeType.GLOBAL },
{ value: "SITE", label: TEXT.enums.scopeType.SITE },
];
const siteOptions = computed(() =>
sites.value.map((site) => ({ value: site.id, label: site.name, disabled: !site.is_active }))
);
const siteActiveMap = computed(() => {
const map: Record<string, boolean> = {};
sites.value.forEach((site) => {
if (site?.id) map[site.id] = !!site.is_active;
});
return map;
});
const isAdmin = computed(() => {
return !!auth.user?.is_admin;
});
const canCreate = computed(() => can("documents.create"));
const canUpdate = computed(() => can("documents.update"));
const canDelete = computed(() => can("documents.delete"));
const isInactiveSite = (siteId?: string | null) => !!siteId && siteActiveMap.value[siteId] === false;
const documentRowClass = ({ row }: { row: DocumentSummary }) =>
[
row?.id ? "clickable-row" : "",
isInactiveSite(row?.site_id) ? "row-inactive" : "",
isDesktop && row?.id === selectedDocumentId.value ? "row-selected" : "",
]
.filter(Boolean)
.join(" ");
const filteredItems = computed(() =>
items.value.filter((item) => {
if (filters.scope_type && item?.scope_type !== filters.scope_type) return false;
if (filters.site_id && item?.site_id !== filters.site_id) return false;
if (filters.doc_type && item?.doc_type !== filters.doc_type) return false;
return true;
})
);
const sortedItems = computed(() =>
[...filteredItems.value].sort((a, b) => Number(isInactiveSite(a?.site_id)) - Number(isInactiveSite(b?.site_id)))
);
const selectedDocument = computed(() =>
sortedItems.value.find((item) => item.id === selectedDocumentId.value) || null
);
const editorForm = reactive({
doc_no: "",
title: "",
scope_type: "GLOBAL",
site_id: "",
doc_type: "",
});
const editorDirtyGuard = useDrawerDirtyGuard(() => editorForm);
const editorRules: FormRules = {
doc_no: [{ required: true, message: TEXT.common.messages.required, trigger: "blur" }],
title: [{ required: true, message: TEXT.common.messages.required, trigger: "blur" }],
scope_type: [{ required: true, message: TEXT.common.messages.required, trigger: "change" }],
site_id: [
{
validator: (_rule, value, callback) => {
if (editorForm.scope_type === "SITE" && !value) {
callback(new Error(TEXT.common.messages.required));
return;
}
callback();
},
trigger: "change",
},
],
doc_type: [{ required: true, message: TEXT.common.messages.required, trigger: "blur" }],
};
const displaySite = (siteId?: string | null) => {
if (!siteId) return TEXT.common.fallback;
return sites.value.find((s) => s.id === siteId)?.name || siteId;
};
const ensureTrialRoute = () => {
if (trialId.value) return;
const currentStudy = study.currentStudy?.id;
if (currentStudy) {
router.replace(`/trial/${currentStudy}/documents`);
}
};
const loadSites = async () => {
if (!trialId.value) return;
try {
const { data } = await fetchSites(trialId.value, { limit: 500 });
const items = Array.isArray(data) ? data : data.items || [];
sites.value = items;
} catch {
sites.value = [];
}
};
const load = async () => {
if (!trialId.value) return;
loading.value = true;
try {
const { data } = await fetchDocuments({ trial_id: trialId.value });
items.value = data.items || [];
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
} finally {
loading.value = false;
}
};
const resetFilters = () => {
filters.scope_type = "";
filters.site_id = "";
filters.doc_type = "";
};
const resetEditorForm = () => {
editorForm.doc_no = "";
editorForm.title = "";
editorForm.scope_type = "GLOBAL";
editorForm.site_id = "";
editorForm.doc_type = "";
editorFormRef.value?.clearValidate();
};
const openCreate = () => {
if (!canCreate.value) {
ElMessage.warning("权限不足");
return;
}
editingDocumentId.value = "";
resetEditorForm();
editorDirtyGuard.syncBaseline();
editorVisible.value = true;
};
const openEdit = (row: DocumentSummary) => {
if (!canUpdate.value) {
ElMessage.warning("权限不足");
return;
}
if (isInactiveSite(row.site_id)) {
ElMessage.warning("中心已停用");
return;
}
editingDocumentId.value = row.id;
Object.assign(editorForm, {
doc_no: "",
title: row.title || "",
scope_type: row.scope_type || "GLOBAL",
site_id: row.site_id || "",
doc_type: row.doc_type || "",
});
editorFormRef.value?.clearValidate();
editorDirtyGuard.syncBaseline();
editorVisible.value = true;
};
const submitEditor = async () => {
if (editingDocumentId.value && !canUpdate.value) {
ElMessage.warning("权限不足");
return;
}
if (!editingDocumentId.value && !canCreate.value) {
ElMessage.warning("权限不足");
return;
}
if (!editorFormRef.value) return;
await editorFormRef.value.validate(async (valid) => {
if (!valid || !trialId.value) return;
saving.value = true;
try {
if (editorForm.scope_type === "SITE" && isInactiveSite(editorForm.site_id)) {
ElMessage.warning("中心已停用");
saving.value = false;
return;
}
const payload = {
site_id: editorForm.scope_type === "SITE" ? editorForm.site_id : null,
title: editorForm.title.trim(),
doc_type: editorForm.doc_type,
scope_type: editorForm.scope_type,
};
if (editingDocumentId.value) {
await updateDocument(editingDocumentId.value, payload);
} else {
await createDocument({
...payload,
trial_id: trialId.value,
doc_no: editorForm.doc_no.trim(),
site_id: editorForm.scope_type === "SITE" ? editorForm.site_id : undefined,
});
}
editorVisible.value = false;
await load();
ElMessage.success(editingDocumentId.value ? TEXT.common.messages.updateSuccess : TEXT.modules.fileVersionManagement.messages.createSuccess);
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
} finally {
saving.value = false;
}
});
};
const goDetail = (id: string) => router.push(`/documents/${id}`);
const selectDocument = (row: DocumentSummary) => {
if (!row?.id) return;
selectedDocumentId.value = row.id;
};
const handleDocumentRowClick = (row: DocumentSummary) => {
if (!row?.id) return;
if (isDesktop) {
selectDocument(row);
return;
}
goDetail(row.id);
};
const openDocumentDetail = (row?: DocumentSummary | null) => {
const target = row?.id ? row : selectedDocument.value;
if (target?.id) goDetail(target.id);
};
const openSelectedDocument = () => {
closeDocumentContextMenu();
openDocumentDetail(selectedDocument.value);
};
const openSelectedDocumentEditor = () => {
const target = selectedDocument.value;
closeDocumentContextMenu();
if (target) openEdit(target);
};
const deleteSelectedDocument = () => {
const target = selectedDocument.value;
closeDocumentContextMenu();
if (target) {
void confirmDelete(target);
}
};
const closeDocumentContextMenu = () => {
if (!documentContextMenu.value.visible) return;
documentContextMenu.value = { visible: false, x: 0, y: 0 };
};
const openDocumentContextMenu = (row: DocumentSummary, _column: unknown, event: MouseEvent) => {
if (!isDesktop || !row?.id) return;
event.preventDefault();
selectDocument(row);
documentContextMenu.value = {
visible: true,
x: Math.min(event.clientX, window.innerWidth - 184),
y: Math.min(event.clientY, window.innerHeight - 132),
};
};
const confirmDelete = async (row: DocumentSummary) => {
if (!canDelete.value) {
ElMessage.warning("权限不足");
return;
}
if (!row?.id) return;
if (isInactiveSite(row.site_id)) {
ElMessage.warning("中心已停用");
return;
}
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
if (!ok) return;
try {
await deleteDocument(row.id);
ElMessage.success(TEXT.common.messages.deleteSuccess);
await load();
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
}
};
const splitDateTime = (value?: string | null) => {
const displayValue = displayDateTime(value);
const [date, time] = displayValue.split(" ");
return { date, time: time || "" };
};
onMounted(() => {
ensureTrialRoute();
load();
loadSites();
desktopRefreshCleanup = onDesktopRefreshCurrentView(() => {
load();
loadSites();
});
});
onBeforeUnmount(() => {
desktopRefreshCleanup?.();
});
watch(
() => trialId.value,
(value, oldValue) => {
if (value && value !== oldValue) {
load();
loadSites();
}
}
);
watch(
() => sortedItems.value.map((item) => item.id).join("|"),
() => {
if (!isDesktop) return;
const rows = sortedItems.value;
if (!rows.length) {
selectedDocumentId.value = "";
return;
}
if (!rows.some((item) => item.id === selectedDocumentId.value)) {
selectedDocumentId.value = rows[0].id;
}
},
{ immediate: true }
);
</script>
<style scoped>
.page {
display: flex;
flex-direction: column;
gap: 0;
}
.filter-form {
display: flex;
width: 100%;
gap: 8px;
align-items: center;
}
.filter-item-form {
margin-bottom: 0 !important;
margin-right: 0 !important;
}
.filter-select-comp {
width: 132px;
}
.filter-spacer {
flex: 1;
}
.header-action-btn {
height: 32px;
padding: 0 12px;
border-radius: 10px;
}
.editor-header {
display: flex;
align-items: center;
}
.editor-title {
color: var(--ctms-text-main);
font-size: 18px;
font-weight: 700;
}
.document-form {
padding: 0 4px;
}
.form-group {
padding: 16px 0;
}
.form-group-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
color: var(--ctms-text-main);
font-size: 15px;
font-weight: 700;
}
.group-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.group-dot-basic {
background: var(--ctms-primary);
}
.full-width {
width: 100%;
}
.drawer-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* ==================== Table Styling (DrugShipments style) ==================== */
.document-table-section {
background: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.04),
0 2px 8px rgba(0, 0, 0, 0.02);
}
.document-workbench {
min-width: 0;
min-height: 0;
}
.document-workbench.is-desktop {
display: grid;
grid-template-columns: minmax(0, 1fr) 304px;
min-height: min(570px, calc(100vh - 178px));
}
.document-table-pane {
min-width: 0;
outline: none;
}
.document-workbench.is-desktop .document-table-pane {
border-right: 1px solid #e3e9f1;
}
.document-preview-pane {
display: flex;
min-width: 0;
min-height: 0;
flex-direction: column;
gap: 16px;
padding: 16px;
background: #f8fafc;
}
.preview-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.preview-kicker {
margin-bottom: 5px;
color: #6b7e95;
font-size: 11px;
font-weight: 800;
}
.preview-title {
display: -webkit-box;
overflow: hidden;
color: #142033;
font-size: 17px;
font-weight: 800;
line-height: 1.35;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.preview-list {
display: grid;
grid-template-columns: 76px minmax(0, 1fr);
gap: 10px 12px;
margin: 0;
}
.preview-list dt {
color: #7b8da3;
font-size: 12px;
font-weight: 700;
}
.preview-list dd {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: #223349;
font-size: 13px;
font-weight: 650;
}
.preview-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: auto;
}
.preview-empty {
display: flex;
min-height: 180px;
align-items: center;
justify-content: center;
color: #7b8da3;
font-size: 13px;
font-weight: 700;
text-align: center;
}
.document-context-menu {
position: fixed;
z-index: 2300;
display: flex;
width: 172px;
flex-direction: column;
gap: 2px;
padding: 6px;
border: 1px solid #cbd7e5;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 14px 38px rgba(15, 23, 42, 0.16);
}
.document-context-menu button {
appearance: none;
display: flex;
align-items: center;
width: 100%;
min-height: 30px;
padding: 0 9px;
border: 0;
border-radius: 6px;
background: transparent;
color: #223349;
cursor: pointer;
font: inherit;
font-size: 12px;
font-weight: 700;
text-align: left;
}
.document-context-menu button:hover:not(:disabled) {
background: #eef4fb;
color: #183756;
}
.document-context-menu button.danger {
color: #b42318;
}
.document-context-menu button:disabled {
cursor: not-allowed;
color: #9aaabd;
}
.ctms-table {
--el-table-border-color: transparent;
--el-table-row-hover-bg-color: #f8f9fb;
}
.ctms-table :deep(.el-table__inner-wrapper::before) {
display: none;
}
.ctms-table :deep(th.el-table__cell) {
background: #f4f5f7;
color: #2a2a2a;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 14px 14px;
border-bottom: 2px solid #e0e0e0 !important;
}
.ctms-table :deep(td.el-table__cell) {
padding: 12px 14px;
color: #0a0a0a;
font-size: 14px;
font-weight: 500;
}
.ctms-table :deep(.el-table__body tr) {
cursor: pointer;
transition: background 0.1s ease;
}
.ctms-table :deep(.el-table__body tr.row-selected > td) {
background: #eaf1f8 !important;
}
.ctms-table :deep(.font-semibold) {
font-weight: 600;
color: #0a0a0a;
}
.ctms-table :deep(.text-secondary) {
color: #8a8a8a;
}
.ctms-table :deep(.datetime-stack) {
display: inline-flex;
flex-direction: column;
gap: 2px;
font-size: 12px;
font-variant-numeric: tabular-nums;
font-weight: 600;
line-height: 1.25;
white-space: nowrap;
}
.ctms-table :deep(.datetime-stack__time) {
color: #9a9a9a;
font-size: 11px;
}
.ctms-table :deep(.el-tag) {
font-weight: 600;
border: none;
}
.ctms-table :deep(.cell-actions) {
display: flex;
gap: 8px;
white-space: nowrap;
}
.ctms-table :deep(.cell-actions .el-button) {
font-size: 13px;
padding: 0;
height: auto;
}
.ctms-table :deep(.cell-actions .el-button + .el-button) {
margin-left: 0;
}
:global([data-ctms-theme="dark"] .document-page--desktop .document-table-section) {
background: #172033;
}
:global([data-ctms-theme="dark"] .document-page--desktop .document-preview-pane) {
border-color: #26364a;
background: #111827;
}
:global([data-ctms-theme="dark"] .document-page--desktop .preview-title),
:global([data-ctms-theme="dark"] .document-page--desktop .preview-list dd) {
color: #f8fafc;
}
:global([data-ctms-theme="dark"] .document-page--desktop .preview-kicker),
:global([data-ctms-theme="dark"] .document-page--desktop .preview-list dt),
:global([data-ctms-theme="dark"] .document-page--desktop .preview-empty) {
color: #94a3b8;
}
:global([data-ctms-theme="dark"] .document-page--desktop .document-context-menu) {
border-color: #334155;
background: #172033;
}
:global([data-ctms-theme="dark"] .document-page--desktop .document-context-menu button) {
color: #dbe5f1;
}
:global([data-ctms-theme="dark"] .document-page--desktop .document-context-menu button:hover:not(:disabled)) {
background: #243247;
color: #bfdbfe;
}
:global([data-ctms-theme="dark"] .document-page--desktop .ctms-table .el-table__body tr.row-selected > td) {
background: #243247 !important;
}
</style>
<style>
.row-inactive td {
color: var(--ctms-text-secondary);
background-color: #fff7d6 !important;
}
.row-inactive .el-tag {
opacity: 0.6;
}
</style>