<script lang="ts" setup>
|
import type { TreeActionType } from '@jnpf/ui';
|
import type { ActionItem, BasicColumn } from '@jnpf/ui/vxeTable';
|
|
import type { Component } from 'vue';
|
|
import type { DmsPermissionResource } from '#/api/x/dms/permission';
|
import type { DmsPersonalFolder, DmsPersonalFolderFile, DmsPersonalFolderType } from '#/api/x/dms/personalFolder';
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { BasicLeftTree, ScrollContainer } from '@jnpf/ui';
|
import { BasicForm, useForm } from '@jnpf/ui/form';
|
import { BasicModal, useModal } from '@jnpf/ui/modal';
|
import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
|
import { downloadByData, formatToDateTime } from '@jnpf/utils';
|
|
import {
|
BookOutlined,
|
DownloadOutlined,
|
FileExcelOutlined,
|
FileImageOutlined,
|
FileOutlined,
|
FilePdfOutlined,
|
FileTextOutlined,
|
FileWordOutlined,
|
FolderOpenOutlined,
|
ReloadOutlined,
|
StarOutlined,
|
} from '@ant-design/icons-vue';
|
import { Breadcrumb, BreadcrumbItem, Radio, RadioGroup } from 'ant-design-vue';
|
|
import {
|
createDmsFileDownloadTicket,
|
downloadDmsFile,
|
evaluatePersonalFolderDownloads,
|
getPersonalFolderFiles,
|
getPersonalFolders,
|
} from '#/api/x/dms/personalFolder';
|
import { onlineUtils } from '#/utils/jnpf';
|
|
import { dmsPreviewErrorMessage, openDmsFilePreview } from '../shared/dmsFilePreview';
|
import FileViewModeSwitch from '../shared/FileViewModeSwitch.vue';
|
import { personalFileDisplayName, personalFileExpired, personalFileSize, personalFileTone } from './personalFolderRules';
|
|
interface FolderTreeNode {
|
children?: FolderTreeNode[];
|
isLeaf?: boolean;
|
key: string;
|
selectable?: boolean;
|
title: string;
|
}
|
|
defineOptions({ name: 'DmsPersonalFolder' });
|
|
const ROOT_KEY = '__personal_root__';
|
const PAGE_SIZE = 100;
|
const DOWNLOAD_EXPIRED_MESSAGE = '下载授权已过期,请重新提交下载申请';
|
const { createMessage } = useMessage();
|
const leftTreeRef = ref<Nullable<TreeActionType>>(null);
|
const folders = ref<DmsPersonalFolder[]>([]);
|
const files = ref<DmsPersonalFolderFile[]>([]);
|
const selectedFolderType = ref<DmsPersonalFolderType>();
|
const selectedFileId = ref('');
|
const keyword = ref('');
|
const loading = ref(false);
|
const treeLoading = ref(false);
|
const viewMode = ref<'details' | 'tiles'>('details');
|
const downloadingFileId = ref('');
|
const downloadAvailability = ref<Record<string, boolean>>({});
|
const currentTime = ref(Date.now());
|
const downloadTarget = ref<DmsPersonalFolderFile>();
|
const downloadFormat = ref<'pdf' | 'word'>('word');
|
const [registerDownloadModal, { openModal: openDownloadModal, closeModal: closeDownloadModal, setModalProps: setDownloadModalProps }] = useModal();
|
let expiryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
function isDownloadUnavailable(file: DmsPersonalFolderFile, now = currentTime.value) {
|
return loading.value || file.lifecycleStatus === 'EXPIRED' || personalFileExpired(file, now) || downloadAvailability.value[file.id] !== true;
|
}
|
|
function scheduleExpiryUpdate() {
|
clearTimeout(expiryTimer);
|
currentTime.value = Date.now();
|
const deadlines = files.value.map((file) => Date.parse(file.validTo || '')).filter((deadline) => Number.isFinite(deadline) && deadline > currentTime.value);
|
if (deadlines.length) {
|
expiryTimer = setTimeout(
|
() => {
|
currentTime.value = Date.now();
|
scheduleExpiryUpdate();
|
},
|
Math.min(Math.min(...deadlines) - currentTime.value + 1, 2_147_483_647),
|
);
|
}
|
}
|
|
function downloadExpired(file: DmsPersonalFolderFile) {
|
if (!personalFileExpired(file)) return false;
|
createMessage.error(DOWNLOAD_EXPIRED_MESSAGE);
|
return true;
|
}
|
|
function chooseDownloadFormat(file: DmsPersonalFolderFile) {
|
if (downloadExpired(file) || isDownloadUnavailable(file, Date.now()) || downloadingFileId.value) return;
|
selectFile(file);
|
downloadTarget.value = file;
|
downloadFormat.value = /\.pdf$/i.test(personalFileDisplayName(file)) ? 'pdf' : 'word';
|
openDownloadModal(true);
|
}
|
|
async function confirmDownload() {
|
if (!downloadTarget.value || downloadingFileId.value) return;
|
setDownloadModalProps({ confirmLoading: true, canFullscreen: false });
|
try {
|
await downloadFile(downloadTarget.value, downloadFormat.value);
|
closeDownloadModal();
|
} finally {
|
setDownloadModalProps({ confirmLoading: false });
|
}
|
}
|
|
let folderRequestId = 0;
|
let fileRequestId = 0;
|
|
const currentFolder = computed(() => folders.value.find((folder) => folder.code === selectedFolderType.value));
|
const selectedFile = computed(() => files.value.find((file) => file.id === selectedFileId.value));
|
const treeData = computed<FolderTreeNode[]>(() => [
|
{
|
children: folders.value.map((folder) => ({ isLeaf: true, key: folder.code, title: folder.name })),
|
key: ROOT_KEY,
|
selectable: false,
|
title: '个人文件夹',
|
},
|
]);
|
const emptyDescription = computed(() => (keyword.value ? '没有匹配的文件' : '此文件夹为空'));
|
|
const columns: BasicColumn[] = [
|
{ title: '标题', dataIndex: 'fileName', minWidth: 260, slots: { default: 'fileName' } },
|
{ title: '文件编号', dataIndex: 'documentNo', width: 170, slots: { default: 'documentNo' } },
|
{ title: '版本号', dataIndex: 'versionLabel', width: 100, slots: { default: 'versionLabel' } },
|
{ title: '上传日期', dataIndex: 'createdAt', width: 120, slots: { default: 'createdAt' } },
|
{ title: '有效期至', dataIndex: 'validTo', width: 165, slots: { default: 'validTo' } },
|
{ title: '文件大小', dataIndex: 'sizeBytes', width: 105, slots: { default: 'sizeBytes' } },
|
];
|
|
const [registerSearchForm, { setFieldsValue }] = useForm({
|
actionColOptions: { flex: 'none', style: { maxWidth: '100%', textAlign: 'left', width: 'max-content' } },
|
baseColProps: { xs: 24, sm: 18, md: 14, lg: 10, xl: 8 },
|
compact: true,
|
labelWidth: 64,
|
schemas: [
|
{
|
field: 'keyword',
|
label: '关键词',
|
component: 'Input',
|
componentProps: {
|
allowClear: true,
|
maxlength: 200,
|
placeholder: '请输入文件名或文件编号',
|
submitOnPressEnter: true,
|
},
|
},
|
],
|
showActionButtonGroup: true,
|
showAdvancedButton: false,
|
});
|
|
const [registerTable] = useVxeTable({
|
actionColumn: { dataIndex: 'action', fixed: 'right', title: '操作', width: 120 },
|
columns,
|
immediate: false,
|
pagination: false,
|
rowKey: 'id',
|
showIndexColumn: false,
|
showTableSetting: false,
|
});
|
|
function folderIcon(key: string): Component {
|
return (
|
{
|
[ROOT_KEY]: FolderOpenOutlined,
|
BORROW: BookOutlined,
|
DOWNLOAD: DownloadOutlined,
|
FAVORITE: StarOutlined,
|
}[key] || FolderOpenOutlined
|
);
|
}
|
|
function fileIcon(file: DmsPermissionResource): Component {
|
const tone = personalFileTone(personalFileDisplayName(file));
|
return (
|
{
|
excel: FileExcelOutlined,
|
file: FileOutlined,
|
image: FileImageOutlined,
|
pdf: FilePdfOutlined,
|
word: FileWordOutlined,
|
}[tone] || FileTextOutlined
|
);
|
}
|
|
function displayDate(value?: string) {
|
return value ? formatToDateTime(value, 'YYYY-MM-DD') : '—';
|
}
|
|
function validUntil(file: DmsPersonalFolderFile) {
|
return file.validTo ? formatToDateTime(file.validTo) : '长期有效';
|
}
|
|
async function loadFolders() {
|
const requestId = ++folderRequestId;
|
treeLoading.value = true;
|
try {
|
const response = await getPersonalFolders();
|
if (requestId !== folderRequestId) return;
|
folders.value = response.data || [];
|
if (!folders.value.some((folder) => folder.code === selectedFolderType.value)) {
|
selectedFolderType.value = folders.value[0]?.code;
|
}
|
await nextTick();
|
if (selectedFolderType.value) leftTreeRef.value?.setSelectedKeys([selectedFolderType.value]);
|
await loadFiles();
|
} catch {
|
if (requestId !== folderRequestId) return;
|
fileRequestId++;
|
folders.value = [];
|
files.value = [];
|
selectedFolderType.value = undefined;
|
selectedFileId.value = '';
|
loading.value = false;
|
createMessage.error('个人文件夹加载失败');
|
} finally {
|
if (requestId === folderRequestId) treeLoading.value = false;
|
}
|
}
|
|
async function loadFiles() {
|
const requestId = ++fileRequestId;
|
const folderType = selectedFolderType.value;
|
selectedFileId.value = '';
|
if (!folderType) {
|
files.value = [];
|
downloadAvailability.value = {};
|
scheduleExpiryUpdate();
|
loading.value = false;
|
return false;
|
}
|
loading.value = true;
|
try {
|
const response = await getPersonalFolderFiles(folderType, {
|
keyword: keyword.value || undefined,
|
pageSize: PAGE_SIZE,
|
});
|
if (requestId !== fileRequestId) return false;
|
const nextFiles = response.data || [];
|
let availability: Record<string, boolean> = {};
|
if (folderType === 'DOWNLOAD' && nextFiles.length) {
|
try {
|
const decisions = await evaluatePersonalFolderDownloads(nextFiles.map((file) => file.id));
|
availability = Object.fromEntries((decisions.data || []).map((decision) => [decision.resourceId, decision.allowed]));
|
} catch {
|
if (requestId === fileRequestId) createMessage.error('下载权限状态加载失败,请刷新重试');
|
}
|
}
|
if (requestId !== fileRequestId) return false;
|
files.value = nextFiles;
|
downloadAvailability.value = availability;
|
scheduleExpiryUpdate();
|
return true;
|
} catch {
|
if (requestId !== fileRequestId) return false;
|
files.value = [];
|
downloadAvailability.value = {};
|
scheduleExpiryUpdate();
|
createMessage.error('文件列表加载失败');
|
return false;
|
} finally {
|
if (requestId === fileRequestId) loading.value = false;
|
}
|
}
|
|
async function handleTreeSelect(key: string) {
|
const folder = folders.value.find((item) => item.code === key);
|
if (!folder || folder.code === selectedFolderType.value) return;
|
selectedFolderType.value = folder.code;
|
keyword.value = '';
|
await setFieldsValue({ keyword: '' });
|
await loadFiles();
|
}
|
|
async function handleSearch(values: Record<string, unknown>) {
|
keyword.value = typeof values?.keyword === 'string' ? values.keyword.trim() : '';
|
await loadFiles();
|
}
|
|
async function handleSearchReset() {
|
keyword.value = '';
|
await loadFiles();
|
}
|
|
function selectFile(file: DmsPersonalFolderFile) {
|
selectedFileId.value = file.id;
|
}
|
|
async function previewFile(file: DmsPersonalFolderFile) {
|
selectFile(file);
|
if (personalFileExpired(file)) return;
|
try {
|
onlineUtils.showLoading('文件预览加载中');
|
await openDmsFilePreview({
|
fileName: personalFileDisplayName(file),
|
mimeType: file.mimeType,
|
resourceId: file.id,
|
resourceType: 'FILE_VERSION',
|
watermarkBusinessTypes: ['gerenWenjianjia'],
|
});
|
} catch (error) {
|
createMessage.error(dmsPreviewErrorMessage(error));
|
} finally {
|
onlineUtils.hideLoading();
|
}
|
}
|
|
async function downloadFile(file: DmsPersonalFolderFile, format: 'pdf' | 'word') {
|
selectFile(file);
|
if (downloadExpired(file) || isDownloadUnavailable(file, Date.now()) || downloadingFileId.value) return;
|
downloadingFileId.value = file.id;
|
let ticketIssued = false;
|
try {
|
const ticketResponse = await createDmsFileDownloadTicket(file.id, {
|
watermarkBusinessTypes: ['gerenWenjianjia'],
|
});
|
const ticket = ticketResponse.data;
|
if (!ticket?.accessTicket || !ticket.fileVersionId || !ticket.fileName) {
|
throw new Error('后端返回的 DMS 下载票据不完整');
|
}
|
ticketIssued = true;
|
downloadAvailability.value = { ...downloadAvailability.value, [file.id]: false };
|
const blob = await downloadDmsFile(ticket.fileVersionId, ticket.accessTicket, format === 'pdf' ? 'pdf' : undefined);
|
const fileName = format === 'pdf' ? `${ticket.fileName.replace(/\.[^.]+$/, '')}.pdf` : ticket.fileName;
|
downloadByData(blob, fileName, format === 'pdf' ? 'application/pdf' : ticket.mimeType);
|
} catch (error) {
|
createMessage.error(personalFileExpired(file) ? DOWNLOAD_EXPIRED_MESSAGE : error instanceof Error && error.message ? error.message : '文件下载失败');
|
} finally {
|
downloadingFileId.value = '';
|
if (ticketIssued) void loadFiles();
|
}
|
}
|
|
function getTableActions(file: DmsPersonalFolderFile): ActionItem[] {
|
const expired = personalFileExpired(file);
|
const actions: ActionItem[] = [
|
{
|
disabled: expired,
|
label: '预览',
|
onClick: () => previewFile(file),
|
},
|
];
|
if (selectedFolderType.value === 'DOWNLOAD') {
|
actions.push({
|
disabled: isDownloadUnavailable(file) || !!downloadingFileId.value,
|
label: '下载',
|
loading: downloadingFileId.value === file.id,
|
onClick: () => chooseDownloadFormat(file),
|
});
|
}
|
return actions;
|
}
|
|
function eventFile(event: any): DmsPersonalFolderFile | undefined {
|
return event?.row || (event?.id ? event : undefined);
|
}
|
|
function handleTableRowClick(event: any) {
|
const file = eventFile(event);
|
if (file) selectFile(file);
|
}
|
|
function handleTableCellDblclick(event: any) {
|
if (event?.column?.field === 'action') return;
|
const file = eventFile(event);
|
if (file) void previewFile(file);
|
}
|
|
async function handleRefresh() {
|
if (await loadFiles()) createMessage.success('已刷新');
|
}
|
|
onMounted(() => void loadFolders());
|
onBeforeUnmount(() => clearTimeout(expiryTimer));
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper personal-folder-page">
|
<div class="jnpf-content-wrapper-left personal-folder-navigation">
|
<BasicLeftTree
|
ref="leftTreeRef"
|
title="个人文件夹"
|
:field-names="{ children: 'children', key: 'key', title: 'title' }"
|
:loading="treeLoading"
|
:show-search="false"
|
show-toolbar
|
:tree-data="treeData"
|
@reload="loadFolders"
|
@select="handleTreeSelect">
|
<template #title="node">
|
<span class="folder-tree-title" :title="node.title">
|
<component :is="folderIcon(String(node.key))" />
|
<span>{{ node.title }}</span>
|
</span>
|
</template>
|
</BasicLeftTree>
|
</div>
|
|
<div class="jnpf-content-wrapper-center">
|
<div class="jnpf-content-wrapper-search-box">
|
<BasicForm @register="registerSearchForm" @reset="handleSearchReset" @submit="handleSearch" />
|
</div>
|
|
<div class="jnpf-content-wrapper-content personal-folder-content">
|
<div class="personal-folder-toolbar">
|
<Breadcrumb aria-label="当前位置">
|
<BreadcrumbItem>个人文件夹</BreadcrumbItem>
|
<BreadcrumbItem>{{ currentFolder?.name || '文件' }}</BreadcrumbItem>
|
</Breadcrumb>
|
<div class="personal-folder-toolbar-actions">
|
<span class="file-count">{{ files.length }} 个文件</span>
|
<span v-if="selectedFile" class="selected-file" :title="personalFileDisplayName(selectedFile)">
|
已选择:{{ personalFileDisplayName(selectedFile) }}
|
</span>
|
<a-tooltip title="刷新">
|
<a-button aria-label="刷新" :loading="loading" @click="handleRefresh"><ReloadOutlined /></a-button>
|
</a-tooltip>
|
<FileViewModeSwitch v-model:value="viewMode" />
|
</div>
|
</div>
|
|
<BasicVxeTable
|
v-show="viewMode === 'details'"
|
:data-source="files"
|
:empty-text="emptyDescription"
|
:loading="loading"
|
:row-config="{ isCurrent: true }"
|
@cell-dblclick="handleTableCellDblclick"
|
@register="registerTable"
|
@row-click="handleTableRowClick">
|
<template #fileName="{ record }">
|
<span
|
class="file-name-trigger"
|
:class="{ selected: selectedFileId === record.id }"
|
role="button"
|
tabindex="0"
|
:aria-selected="selectedFileId === record.id"
|
:title="personalFileDisplayName(record)"
|
@click.stop="selectFile(record)"
|
@dblclick.stop.prevent="previewFile(record)"
|
@keydown.enter.stop.prevent="previewFile(record)">
|
<span class="file-icon"><component :is="fileIcon(record)" /></span>
|
<span>{{ personalFileDisplayName(record) }}</span>
|
</span>
|
</template>
|
<template #documentNo="{ record }">
|
<span :title="record.documentNo || '—'">{{ record.documentNo || '—' }}</span>
|
</template>
|
<template #versionLabel="{ record }">{{ record.versionLabel || `v${record.versionNo ?? '-'}` }}</template>
|
<template #createdAt="{ record }">{{ displayDate(record.createdAt) }}</template>
|
<template #validTo="{ record }">{{ validUntil(record) }}</template>
|
<template #sizeBytes="{ record }">{{ personalFileSize(record.sizeBytes) }}</template>
|
<template #action="{ record }">
|
<TableAction :actions="getTableActions(record)" stop-button-propagation />
|
</template>
|
</BasicVxeTable>
|
|
<ScrollContainer v-show="viewMode === 'tiles'" class="tile-scroll" v-loading="loading">
|
<div v-if="files.length" class="tile-grid" role="listbox" aria-label="个人文件">
|
<button
|
v-for="file in files"
|
:key="file.id"
|
class="file-tile"
|
:class="{ selected: selectedFileId === file.id }"
|
type="button"
|
:aria-selected="selectedFileId === file.id"
|
:title="personalFileDisplayName(file)"
|
@click="selectFile(file)"
|
@dblclick.stop.prevent="previewFile(file)"
|
@keydown.enter.stop.prevent="previewFile(file)">
|
<span class="tile-icon"><component :is="fileIcon(file)" /></span>
|
<span class="tile-name">{{ personalFileDisplayName(file) }}</span>
|
<span class="tile-meta">{{ file.versionLabel || `v${file.versionNo ?? '-'}` }}</span>
|
</button>
|
</div>
|
<jnpf-empty v-else-if="!loading" :description="emptyDescription" />
|
</ScrollContainer>
|
</div>
|
</div>
|
<BasicModal title="选择下载类型" :width="420" :can-fullscreen="false" ok-text="下载" @register="registerDownloadModal" @ok="confirmDownload">
|
<p>{{ downloadTarget ? personalFileDisplayName(downloadTarget) : '' }}</p>
|
<RadioGroup v-model:value="downloadFormat" aria-label="下载类型">
|
<Radio value="word" :disabled="!!downloadTarget && /\.pdf$/i.test(personalFileDisplayName(downloadTarget))">Word</Radio>
|
<Radio value="pdf">PDF</Radio>
|
</RadioGroup>
|
</BasicModal>
|
</div>
|
</template>
|
|
<style lang="scss" scoped>
|
.personal-folder-page,
|
.personal-folder-content {
|
min-width: 0;
|
}
|
|
.personal-folder-navigation {
|
min-width: 0;
|
}
|
|
.folder-tree-title {
|
display: flex;
|
gap: 8px;
|
align-items: center;
|
width: 100%;
|
min-width: 0;
|
|
> .anticon {
|
display: inline-flex;
|
flex: 0 0 1em;
|
align-items: center;
|
justify-content: center;
|
width: 1em;
|
height: 1em;
|
}
|
|
> span:last-child {
|
flex: 1 1 auto;
|
min-width: 0;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
}
|
|
.personal-folder-content {
|
display: flex;
|
flex-direction: column;
|
background: var(--component-background);
|
}
|
|
.personal-folder-toolbar {
|
display: flex;
|
flex: 0 0 52px;
|
gap: 16px;
|
align-items: center;
|
justify-content: space-between;
|
min-width: 0;
|
padding: 0 10px;
|
border-bottom: 1px solid var(--border-color-base1);
|
}
|
|
.personal-folder-toolbar-actions {
|
display: flex;
|
flex: 0 1 auto;
|
gap: 10px;
|
align-items: center;
|
min-width: 0;
|
font-size: 12px;
|
color: var(--text-color-secondary);
|
}
|
|
.file-count {
|
flex: 0 0 auto;
|
}
|
|
.selected-file {
|
max-width: 240px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
:deep(.jnpf-vxe-table) {
|
flex: 1;
|
min-width: 0;
|
min-height: 0;
|
}
|
|
.file-name-trigger {
|
display: flex;
|
gap: 8px;
|
align-items: center;
|
width: 100%;
|
min-width: 0;
|
cursor: default;
|
outline: none;
|
|
> span:last-child {
|
flex: 1 1 auto;
|
min-width: 0;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
&.selected,
|
&:focus-visible {
|
color: var(--primary-color);
|
}
|
|
&:focus-visible {
|
outline: 1px solid var(--primary-color);
|
outline-offset: 2px;
|
}
|
}
|
|
.file-icon,
|
.tile-icon {
|
display: inline-flex;
|
flex: 0 0 auto;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.file-icon {
|
width: 20px;
|
font-size: 18px;
|
}
|
|
.tile-scroll {
|
flex: 1;
|
min-height: 0;
|
}
|
|
.tile-grid {
|
display: grid;
|
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
gap: 8px;
|
padding: 12px;
|
}
|
|
.file-tile {
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
align-items: center;
|
justify-content: center;
|
min-width: 0;
|
height: 126px;
|
padding: 10px;
|
color: inherit;
|
text-align: center;
|
background: transparent;
|
border: 1px solid transparent;
|
border-radius: var(--radius);
|
|
&:hover,
|
&:focus-visible {
|
background: var(--app-content-background);
|
border-color: var(--border-color-base1);
|
outline: none;
|
}
|
|
&.selected {
|
background: color-mix(in srgb, var(--primary-color) 10%, var(--component-background));
|
border-color: color-mix(in srgb, var(--primary-color) 38%, var(--border-color-base1));
|
}
|
}
|
|
.tile-icon {
|
height: 50px;
|
font-size: 44px;
|
}
|
|
.tile-name,
|
.tile-meta {
|
width: 100%;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.tile-name {
|
font-size: 13px;
|
}
|
|
.tile-meta {
|
font-size: 12px;
|
color: var(--text-color-secondary);
|
}
|
|
@media (max-width: 680px) {
|
.personal-folder-page {
|
flex-direction: column;
|
}
|
|
.personal-folder-navigation {
|
width: 100%;
|
height: 176px;
|
margin-right: 0;
|
margin-bottom: 10px;
|
}
|
|
.jnpf-content-wrapper-center {
|
min-height: 420px;
|
}
|
|
.selected-file {
|
display: none;
|
}
|
|
.personal-folder-toolbar {
|
gap: 8px;
|
}
|
}
|
</style>
|