费用管理内容优化-初步
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<el-card class="kpi-card" shadow="never">
|
||||
<el-card class="kpi-card" :class="[`type-${type}`]" shadow="hover">
|
||||
<div class="kpi-header">
|
||||
<span class="kpi-title">{{ title }}</span>
|
||||
<el-icon v-if="icon" class="kpi-icon"><component :is="icon" /></el-icon>
|
||||
<div class="kpi-title-wrapper">
|
||||
<div class="kpi-icon-wrapper" v-if="icon">
|
||||
<el-icon class="kpi-icon"><component :is="icon" /></el-icon>
|
||||
</div>
|
||||
<span class="kpi-title">{{ title }}</span>
|
||||
</div>
|
||||
<slot name="header-suffix"></slot>
|
||||
</div>
|
||||
<div class="kpi-content">
|
||||
<div v-if="loading" class="kpi-loading">
|
||||
@@ -22,6 +27,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
value: string | number;
|
||||
@@ -29,38 +36,85 @@ interface Props {
|
||||
loading?: boolean;
|
||||
icon?: any;
|
||||
unit?: string;
|
||||
type?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'default',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kpi-card {
|
||||
border: 1px solid var(--ctms-border-color);
|
||||
transition: var(--ctms-transition);
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
--el-card-padding: 12px 16px;
|
||||
}
|
||||
|
||||
.kpi-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.kpi-card.type-primary::before { background-color: var(--el-color-primary); }
|
||||
.kpi-card.type-success::before { background-color: var(--el-color-success); }
|
||||
.kpi-card.type-warning::before { background-color: var(--el-color-warning); }
|
||||
.kpi-card.type-danger::before { background-color: var(--el-color-danger); }
|
||||
.kpi-card.type-info::before { background-color: var(--el-color-info); }
|
||||
|
||||
.kpi-card:hover {
|
||||
border-color: var(--ctms-border-color-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.kpi-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kpi-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kpi-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--ctms-bg-color-page);
|
||||
color: var(--ctms-text-secondary);
|
||||
}
|
||||
|
||||
.type-primary .kpi-icon-wrapper { color: var(--el-color-primary); background-color: var(--el-color-primary-light-9); }
|
||||
.type-success .kpi-icon-wrapper { color: var(--el-color-success); background-color: var(--el-color-success-light-9); }
|
||||
.type-warning .kpi-icon-wrapper { color: var(--el-color-warning); background-color: var(--el-color-warning-light-9); }
|
||||
.type-danger .kpi-icon-wrapper { color: var(--el-color-danger); background-color: var(--el-color-danger-light-9); }
|
||||
.type-info .kpi-icon-wrapper { color: var(--el-color-info); background-color: var(--el-color-info-light-9); }
|
||||
|
||||
.kpi-title {
|
||||
color: var(--ctms-text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.kpi-icon {
|
||||
font-size: 16px;
|
||||
color: var(--ctms-text-secondary);
|
||||
}
|
||||
|
||||
.kpi-content {
|
||||
@@ -75,24 +129,26 @@ defineProps<Props>();
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--ctms-text-main);
|
||||
line-height: 1.2;
|
||||
font-family: var(--el-font-family);
|
||||
}
|
||||
|
||||
.kpi-unit {
|
||||
font-size: 14px;
|
||||
color: var(--ctms-text-secondary);
|
||||
font-weight: 500;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.kpi-subtext {
|
||||
color: var(--ctms-text-secondary);
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--ctms-border-color);
|
||||
border-top: 1px solid var(--ctms-border-color-lighter);
|
||||
}
|
||||
|
||||
.kpi-loading {
|
||||
|
||||
@@ -39,13 +39,13 @@
|
||||
<el-icon><House /></el-icon>
|
||||
<span>{{ TEXT.menu.projectOverview }}</span>
|
||||
</el-menu-item>
|
||||
<el-sub-menu index="finance">
|
||||
<el-sub-menu index="fees">
|
||||
<template #title>
|
||||
<el-icon><Coin /></el-icon>
|
||||
<span>{{ TEXT.menu.finance }}</span>
|
||||
</template>
|
||||
<el-menu-item index="/finance/contracts">{{ TEXT.menu.financeContracts }}</el-menu-item>
|
||||
<el-menu-item index="/finance/special">{{ TEXT.menu.financeSpecials }}</el-menu-item>
|
||||
<el-menu-item index="/fees/contracts">{{ TEXT.menu.feeContracts }}</el-menu-item>
|
||||
<el-menu-item index="/fees/special">{{ TEXT.menu.feeSpecials }}</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="drug">
|
||||
<template #title>
|
||||
@@ -170,8 +170,10 @@ const isCollapsed = ref(localStorage.getItem("ctms_sidebar_collapsed") === "1");
|
||||
const activeMenu = computed(() => {
|
||||
const path = route.path;
|
||||
if (path.startsWith("/project/")) return "/project/overview";
|
||||
if (path.startsWith("/finance/contracts")) return "/finance/contracts";
|
||||
if (path.startsWith("/finance/special")) return "/finance/special";
|
||||
if (path.startsWith("/fees/contracts")) return "/fees/contracts";
|
||||
if (path.startsWith("/fees/special")) return "/fees/special";
|
||||
if (path.startsWith("/finance/contracts")) return "/fees/contracts";
|
||||
if (path.startsWith("/finance/special")) return "/fees/special";
|
||||
if (path.startsWith("/drug/shipments")) return "/drug/shipments";
|
||||
if (path.startsWith("/file-versions")) return "/file-versions";
|
||||
if (path.startsWith("/startup/feasibility") || path.startsWith("/startup/ethics")) return "/startup/feasibility-ethics";
|
||||
|
||||
@@ -36,7 +36,7 @@ const actions = [
|
||||
{ label: TEXT.menu.startupMeetingAuth, path: "/startup/meeting-auth", icon: Timer },
|
||||
{ label: TEXT.menu.subjects, path: "/subjects", icon: UserFilled },
|
||||
{ label: TEXT.menu.drugShipments, path: "/drug/shipments", icon: Management },
|
||||
{ label: TEXT.menu.finance, path: "/finance/contracts", icon: Money },
|
||||
{ label: TEXT.menu.finance, path: "/fees/contracts", icon: Money },
|
||||
{ label: TEXT.menu.knowledge, path: "/knowledge/medical-consult", icon: Collection },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="state state-error">
|
||||
<el-icon class="state-icon"><CircleCloseFilled /></el-icon>
|
||||
<el-icon class="state-icon"><CircleClose /></el-icon>
|
||||
<div class="state-title">{{ title }}</div>
|
||||
<div v-if="description" class="state-desc">{{ description }}</div>
|
||||
<div v-if="$slots.action" class="state-action">
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CircleCloseFilled } from "@element-plus/icons-vue";
|
||||
import { CircleClose } from "@element-plus/icons-vue";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
withDefaults(
|
||||
@@ -32,6 +32,7 @@ withDefaults(
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 12px;
|
||||
color: var(--ctms-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,11 @@
|
||||
{{ displayDateTime(scope.row.uploaded_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="180">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="220">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="preview(scope.row)">
|
||||
{{ TEXT.common.actions.view }}
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="download(scope.row)">{{ TEXT.common.actions.download }}</el-button>
|
||||
<el-button
|
||||
v-if="canDelete(scope.row)"
|
||||
@@ -40,6 +43,19 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="previewVisible" :title="previewTitle" width="720px" :close-on-click-modal="false">
|
||||
<div v-if="previewError" class="preview-error">
|
||||
{{ previewError }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<img v-if="previewType === 'image'" :src="previewUrl" class="preview-media" />
|
||||
<iframe v-else-if="previewType === 'pdf'" :src="previewUrl" class="preview-frame" />
|
||||
<div v-else class="preview-error">
|
||||
{{ TEXT.common.messages.previewNotSupported }}
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -65,6 +81,11 @@ const loading = ref(false);
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
const members = ref<any[]>([]);
|
||||
const previewVisible = ref(false);
|
||||
const previewUrl = ref("");
|
||||
const previewType = ref<"image" | "pdf" | "other">("other");
|
||||
const previewTitle = ref(TEXT.common.labels.preview);
|
||||
const previewError = ref("");
|
||||
|
||||
const load = async () => {
|
||||
if (!props.studyId || !props.entityId) return;
|
||||
@@ -107,6 +128,25 @@ const download = (row: any) => {
|
||||
window.open(url, "_blank");
|
||||
};
|
||||
|
||||
const detectPreviewType = (row: any) => {
|
||||
const mime = (row?.mime_type || row?.content_type || "").toLowerCase();
|
||||
if (mime.startsWith("image/")) return "image";
|
||||
if (mime === "application/pdf") return "pdf";
|
||||
return "other";
|
||||
};
|
||||
|
||||
const preview = (row: any) => {
|
||||
previewError.value = "";
|
||||
previewType.value = detectPreviewType(row);
|
||||
previewTitle.value = row?.filename || TEXT.common.labels.preview;
|
||||
const token = localStorage.getItem("ctms_token");
|
||||
previewUrl.value = row?.url || (token ? `/api/v1/attachments/${row.id}/download?token=${token}` : `/api/v1/attachments/${row.id}/download`);
|
||||
if (previewType.value === "other") {
|
||||
previewError.value = TEXT.common.messages.previewNotSupported;
|
||||
}
|
||||
previewVisible.value = true;
|
||||
};
|
||||
|
||||
const canDelete = (row: any) => {
|
||||
const userId = auth.user?.id;
|
||||
const role = auth.user?.role;
|
||||
@@ -141,4 +181,24 @@ onMounted(async () => {
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.preview-frame {
|
||||
width: 100%;
|
||||
height: 520px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.preview-media {
|
||||
max-width: 100%;
|
||||
max-height: 520px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.preview-error {
|
||||
color: var(--ctms-text-secondary);
|
||||
font-size: 13px;
|
||||
padding: 12px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div class="fee-attachments">
|
||||
<el-card v-for="group in groups" :key="group.key" class="group-card">
|
||||
<div class="group-header">
|
||||
<div>
|
||||
<div class="group-title">{{ group.label }}</div>
|
||||
<div v-if="group.description" class="group-desc">{{ group.description }}</div>
|
||||
</div>
|
||||
<el-upload
|
||||
:http-request="(options: any) => doUpload(group.key, options)"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
:disabled="readonly || isUploading(group.key)"
|
||||
:auto-upload="true"
|
||||
>
|
||||
<el-button size="small" type="primary" :loading="isUploading(group.key)" :disabled="readonly">
|
||||
{{ TEXT.common.actions.upload }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<el-progress
|
||||
v-if="progressMap[group.key] && progressMap[group.key] < 100"
|
||||
:percentage="progressMap[group.key]"
|
||||
:stroke-width="6"
|
||||
/>
|
||||
<StateLoading v-if="loading" :rows="3" />
|
||||
<template v-else>
|
||||
<div v-if="(grouped[group.key] || []).length === 0" class="group-empty">
|
||||
<StateEmpty :description="group.emptyText" />
|
||||
</div>
|
||||
<el-table v-else :data="grouped[group.key]" style="width: 100%">
|
||||
<el-table-column prop="filename" :label="TEXT.common.labels.filename" min-width="160" />
|
||||
<el-table-column :label="TEXT.common.labels.size" width="110">
|
||||
<template #default="scope">{{ formatFileSize(scope.row.size || scope.row.file_size) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.uploader" width="140">
|
||||
<template #default="scope">
|
||||
{{ uploaderLabel(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="160">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="preview(scope.row)">
|
||||
{{ TEXT.common.actions.view }}
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="download(scope.row)">
|
||||
{{ TEXT.common.actions.download }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDelete(scope.row)"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="remove(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="previewVisible" :title="previewTitle" width="720px" :close-on-click-modal="false">
|
||||
<div v-if="previewError" class="preview-error">
|
||||
{{ previewError }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<img v-if="previewType === 'image'" :src="previewUrl" class="preview-media" />
|
||||
<iframe v-else-if="previewType === 'pdf'" :src="previewUrl" class="preview-frame" />
|
||||
<div v-else class="preview-error">
|
||||
{{ TEXT.common.messages.previewNotSupported }}
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { listFeeAttachments, uploadFeeAttachment, deleteFeeAttachment, getFeeAttachmentDownloadUrl } from "../../api/feeAttachments";
|
||||
import { listMembers } from "../../api/members";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { formatFileSize } from "../attachments/attachmentUtils";
|
||||
import { displayUser, getMemberDisplayName, getUserDisplayName } from "../../utils/display";
|
||||
import StateEmpty from "../StateEmpty.vue";
|
||||
import StateLoading from "../StateLoading.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
interface AttachmentGroup {
|
||||
key: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
groups: AttachmentGroup[];
|
||||
readonly?: boolean;
|
||||
maxSizeMb?: number;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const grouped = reactive<Record<string, any[]>>({});
|
||||
const progressMap = reactive<Record<string, number>>({});
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
const members = ref<any[]>([]);
|
||||
const previewVisible = ref(false);
|
||||
const previewUrl = ref("");
|
||||
const previewType = ref<"image" | "pdf" | "other">("other");
|
||||
const previewTitle = ref(TEXT.common.labels.preview);
|
||||
const previewError = ref("");
|
||||
|
||||
const isUploading = (key: string) => (progressMap[key] || 0) > 0 && (progressMap[key] || 0) < 100;
|
||||
|
||||
const loadMembers = async () => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId) return;
|
||||
try {
|
||||
const { data } = await listMembers(studyId, { limit: 500 });
|
||||
members.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch {
|
||||
members.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
if (!props.entityType || !props.entityId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await listFeeAttachments(props.entityType, props.entityId);
|
||||
const items = Array.isArray(data?.data) ? data?.data : [];
|
||||
props.groups.forEach((group) => {
|
||||
grouped[group.key] = items.filter((item: any) => item.file_type === group.key || item.fileType === group.key);
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const uploaderLabel = (row: any) => {
|
||||
const memberMap = members.value.reduce<Record<string, string>>((acc, cur) => {
|
||||
const username = getMemberDisplayName(cur);
|
||||
if (cur?.user_id && username) acc[cur.user_id] = username;
|
||||
return acc;
|
||||
}, {});
|
||||
if (row?.uploaded_by && typeof row.uploaded_by === "object") {
|
||||
return getUserDisplayName(row.uploaded_by) || row.uploaded_by.id || TEXT.common.fallback;
|
||||
}
|
||||
return displayUser(row.uploaded_by_id || row.uploaded_by, { members: memberMap });
|
||||
};
|
||||
|
||||
const doUpload = async (fileType: string, options: any) => {
|
||||
const file: File = options.file;
|
||||
if (!file || props.readonly) return;
|
||||
const maxSize = props.maxSizeMb ?? 50;
|
||||
if (file.size > maxSize * 1024 * 1024) {
|
||||
ElMessage.error(`${TEXT.common.messages.fileTooLarge} ${maxSize}MB`);
|
||||
return;
|
||||
}
|
||||
progressMap[fileType] = 0;
|
||||
try {
|
||||
await uploadFeeAttachment(props.entityType, props.entityId, fileType, file, {
|
||||
onUploadProgress: (evt: ProgressEvent) => {
|
||||
if (evt.total) {
|
||||
progressMap[fileType] = Math.round((evt.loaded / evt.total) * 100);
|
||||
}
|
||||
},
|
||||
});
|
||||
ElMessage.success(TEXT.common.messages.uploadSuccess);
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.uploadFailed);
|
||||
} finally {
|
||||
progressMap[fileType] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const detectPreviewType = (row: any) => {
|
||||
const mime = (row?.mime_type || row?.content_type || "").toLowerCase();
|
||||
if (mime.startsWith("image/")) return "image";
|
||||
if (mime === "application/pdf") return "pdf";
|
||||
return "other";
|
||||
};
|
||||
|
||||
const preview = (row: any) => {
|
||||
previewError.value = "";
|
||||
previewType.value = detectPreviewType(row);
|
||||
previewTitle.value = row?.filename || TEXT.common.labels.preview;
|
||||
previewUrl.value = row?.url || getFeeAttachmentDownloadUrl(row.id);
|
||||
if (previewType.value === "other") {
|
||||
previewError.value = TEXT.common.messages.previewNotSupported;
|
||||
}
|
||||
previewVisible.value = true;
|
||||
};
|
||||
|
||||
const download = (row: any) => {
|
||||
if (row?.url) {
|
||||
window.open(row.url, "_blank");
|
||||
return;
|
||||
}
|
||||
window.open(getFeeAttachmentDownloadUrl(row.id), "_blank");
|
||||
};
|
||||
|
||||
const canDelete = (row: any) => {
|
||||
if (props.readonly) return false;
|
||||
const userId = auth.user?.id;
|
||||
const role = auth.user?.role;
|
||||
const projectRole = study.currentStudyRole || (study.currentStudy as any)?.role_in_study;
|
||||
const ownerId =
|
||||
row.uploaded_by_id || (row.uploaded_by && typeof row.uploaded_by === "object" ? row.uploaded_by.id : row.uploaded_by);
|
||||
return userId === ownerId || role === "ADMIN" || projectRole === "PM";
|
||||
};
|
||||
|
||||
const remove = async (row: any) => {
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteFeeAttachment(row.id);
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
load();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.entityType, props.entityId],
|
||||
() => {
|
||||
load();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMembers();
|
||||
load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fee-attachments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-text-main);
|
||||
}
|
||||
|
||||
.group-desc {
|
||||
font-size: 12px;
|
||||
color: var(--ctms-text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.group-empty {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Compact Empty State Override */
|
||||
.group-empty :deep(.state) {
|
||||
padding: 16px 0 !important;
|
||||
min-height: auto !important;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.group-empty :deep(.state-icon) {
|
||||
font-size: 20px !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.group-empty :deep(.state-title) {
|
||||
font-size: 13px !important;
|
||||
font-weight: normal !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.group-empty :deep(.state-desc) {
|
||||
display: none; /* Hide description in compact mode if desired, or keep it small */
|
||||
}
|
||||
|
||||
.preview-frame {
|
||||
width: 100%;
|
||||
height: 520px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.preview-media {
|
||||
max-width: 100%;
|
||||
max-height: 520px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.preview-error {
|
||||
color: var(--ctms-text-secondary);
|
||||
font-size: 13px;
|
||||
padding: 12px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user