✨ 优化 2026 主题移动端与详情体验
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="p-4 sm:p-6">
|
||||
<div class="mb-6 flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" :class="[primaryTextClass]">
|
||||
@@ -11,22 +11,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="mb-5 grid grid-cols-2 gap-3 sm:mb-6 sm:gap-4 xl:grid-cols-4">
|
||||
<div
|
||||
v-for="card in summaryCards"
|
||||
:key="card.label"
|
||||
class="theme-2026-card theme-2026-card-hover group rounded-[30px] border p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
class="theme-2026-card theme-2026-card-hover group rounded-[24px] border p-3.5 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg sm:rounded-[30px] sm:p-4"
|
||||
:class="[panelClass, cardHoverClass]"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm" :class="[mutedTextClass]">{{ card.label }}</p>
|
||||
<p class="mt-2 text-2xl font-semibold" :class="[primaryTextClass]">
|
||||
<p class="text-xs sm:text-sm" :class="[mutedTextClass]">{{ card.label }}</p>
|
||||
<p class="mt-1.5 truncate text-xl font-semibold tabular-nums sm:mt-2 sm:text-2xl" :class="[primaryTextClass]">
|
||||
{{ card.value }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg p-2 transition-transform duration-200 group-hover:scale-105"
|
||||
class="rounded-lg p-2 transition-transform duration-200 group-hover:scale-105 max-sm:p-1.5"
|
||||
:class="[card.iconClass]"
|
||||
>
|
||||
<component :is="card.icon" class="h-5 w-5" />
|
||||
@@ -39,14 +39,14 @@
|
||||
class="theme-2026-card theme-2026-card-hover mb-6 rounded-[30px] border p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
:class="[panelClass, cardHoverClass]"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold" :class="[primaryTextClass]">条件筛选</h3>
|
||||
<p class="mt-1 text-xs" :class="[mutedTextClass]">
|
||||
{{ hasActiveFilters ? '已应用筛选条件' : '默认收起,需要时展开' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2 sm:justify-end">
|
||||
<BaseButton
|
||||
v-if="hasActiveFilters"
|
||||
variant="outline"
|
||||
@@ -269,10 +269,10 @@
|
||||
v-if="tableData.length > 0"
|
||||
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<label class="inline-flex items-center gap-2 text-sm" :class="[primaryTextClass]">
|
||||
<label class="inline-flex min-h-11 cursor-pointer items-center gap-2 text-sm sm:min-h-0" :class="[primaryTextClass]">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
:checked="isAllCurrentPageSelected"
|
||||
:disabled="isBatchActionRunning"
|
||||
:indeterminate="isCurrentPagePartiallySelected"
|
||||
@@ -287,7 +287,7 @@
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div v-if="hasSelectedFiles" class="flex flex-wrap items-center gap-2">
|
||||
<BaseButton
|
||||
v-if="hasSelectedFiles"
|
||||
variant="outline"
|
||||
@@ -407,7 +407,7 @@
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
:checked="selectedFileIds.has(file.id)"
|
||||
:disabled="isBatchActionRunning"
|
||||
:aria-label="t('fileManage.selectFile', { name: file.displayName })"
|
||||
@@ -873,70 +873,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="theme-2026-card-hover space-y-3 rounded-[30px] border px-4 py-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
:class="[subtleSectionClass, cardHoverClass]"
|
||||
>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||
{{ t('fileManage.metadataInfo') }}
|
||||
</h4>
|
||||
<p class="mt-1 text-xs" :class="[mutedTextClass]">
|
||||
{{ t('fileManage.metadataHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<BaseButton
|
||||
size="sm"
|
||||
:loading="isDetailMetadataSaving"
|
||||
:disabled="isDetailLoading"
|
||||
@click="updateDetailMetadata"
|
||||
>
|
||||
<template #icon>
|
||||
<CheckIcon class="mr-2 h-4 w-4" />
|
||||
</template>
|
||||
{{ t('fileManage.saveMetadata') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(220px,0.55fr)]">
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-medium" :class="[mutedTextClass]">
|
||||
{{ t('fileManage.metadataNote') }}
|
||||
</span>
|
||||
<textarea
|
||||
v-model="detailMetadataForm.note"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
class="w-full resize-y rounded-lg border px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-indigo-500"
|
||||
:class="[fieldClass]"
|
||||
:placeholder="t('fileManage.metadataNotePlaceholder')"
|
||||
></textarea>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-medium" :class="[mutedTextClass]">
|
||||
{{ t('fileManage.metadataTags') }}
|
||||
</span>
|
||||
<input
|
||||
v-model="detailMetadataForm.tagsText"
|
||||
type="text"
|
||||
class="w-full rounded-lg border px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-indigo-500"
|
||||
:class="[fieldClass]"
|
||||
:placeholder="t('fileManage.metadataTagsPlaceholder')"
|
||||
/>
|
||||
<span class="block text-xs" :class="[mutedTextClass]">
|
||||
{{
|
||||
selectedFileDetail.metadataUpdatedAt
|
||||
? t('fileManage.metadataUpdatedAt', {
|
||||
time: formatMetadataUpdatedAt(selectedFileDetail.metadataUpdatedAt)
|
||||
})
|
||||
: t('fileManage.metadataNeverUpdated')
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="theme-2026-card-hover rounded-[30px] border px-4 py-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
:class="getInsightPanelClass(selectedFileDetail.statusInsightSeverity)"
|
||||
@@ -1046,15 +982,32 @@
|
||||
</section>
|
||||
|
||||
<section v-if="selectedFileDetail.detailTimeline.length > 0" class="space-y-3">
|
||||
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||
{{ t('fileManage.lifecycle') }}
|
||||
</h4>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||
{{ t('fileManage.lifecycle') }}
|
||||
</h4>
|
||||
<button
|
||||
v-if="selectedFileDetail.detailTimeline.length > 3"
|
||||
type="button"
|
||||
class="inline-flex min-h-11 items-center self-start rounded-lg border px-3 py-2 text-sm font-medium transition-colors sm:min-h-0 sm:self-auto"
|
||||
:class="detailActionClass"
|
||||
@click="isLifecycleExpanded = !isLifecycleExpanded"
|
||||
>
|
||||
{{
|
||||
isLifecycleExpanded
|
||||
? t('fileManage.collapseLifecycle')
|
||||
: t('fileManage.expandLifecycle', {
|
||||
count: selectedFileDetail.detailTimeline.length
|
||||
})
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-[30px] border px-4 py-4 shadow-md"
|
||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-700/30' : 'border-gray-200 bg-white']"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in selectedFileDetail.detailTimeline"
|
||||
v-for="(item, index) in visibleDetailTimeline"
|
||||
:key="`${item.key}-${index}`"
|
||||
class="grid grid-cols-[auto_minmax(0,1fr)] gap-3"
|
||||
>
|
||||
@@ -1064,7 +1017,7 @@
|
||||
:class="getTimelineDotClass(item.severity)"
|
||||
/>
|
||||
<span
|
||||
v-if="index < selectedFileDetail.detailTimeline.length - 1"
|
||||
v-if="index < visibleDetailTimeline.length - 1"
|
||||
class="my-1 w-px flex-1"
|
||||
:class="[isDarkMode ? 'bg-gray-600' : 'bg-gray-200']"
|
||||
/>
|
||||
@@ -1091,6 +1044,70 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="theme-2026-card-hover space-y-3 rounded-[30px] border px-4 py-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
:class="[subtleSectionClass, cardHoverClass]"
|
||||
>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||
{{ t('fileManage.metadataInfo') }}
|
||||
</h4>
|
||||
<p class="mt-1 text-xs" :class="[mutedTextClass]">
|
||||
{{ t('fileManage.metadataHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<BaseButton
|
||||
size="sm"
|
||||
:loading="isDetailMetadataSaving"
|
||||
:disabled="isDetailLoading"
|
||||
@click="updateDetailMetadata"
|
||||
>
|
||||
<template #icon>
|
||||
<CheckIcon class="mr-2 h-4 w-4" />
|
||||
</template>
|
||||
{{ t('fileManage.saveMetadata') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(220px,0.55fr)]">
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-medium" :class="[mutedTextClass]">
|
||||
{{ t('fileManage.metadataNote') }}
|
||||
</span>
|
||||
<textarea
|
||||
v-model="detailMetadataForm.note"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
class="w-full resize-y rounded-lg border px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-indigo-500"
|
||||
:class="[fieldClass]"
|
||||
:placeholder="t('fileManage.metadataNotePlaceholder')"
|
||||
></textarea>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-medium" :class="[mutedTextClass]">
|
||||
{{ t('fileManage.metadataTags') }}
|
||||
</span>
|
||||
<input
|
||||
v-model="detailMetadataForm.tagsText"
|
||||
type="text"
|
||||
class="w-full rounded-lg border px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-indigo-500"
|
||||
:class="[fieldClass]"
|
||||
:placeholder="t('fileManage.metadataTagsPlaceholder')"
|
||||
/>
|
||||
<span class="block text-xs" :class="[mutedTextClass]">
|
||||
{{
|
||||
selectedFileDetail.metadataUpdatedAt
|
||||
? t('fileManage.metadataUpdatedAt', {
|
||||
time: formatMetadataUpdatedAt(selectedFileDetail.metadataUpdatedAt)
|
||||
})
|
||||
: t('fileManage.metadataNeverUpdated')
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||
{{ t('fileManage.storageInfo') }}
|
||||
@@ -1572,6 +1589,11 @@ const {
|
||||
} = useAdminFiles()
|
||||
|
||||
const isFilterPanelOpen = ref(false)
|
||||
const isLifecycleExpanded = ref(false)
|
||||
const visibleDetailTimeline = computed(() => {
|
||||
const rows = selectedFileDetail.value?.detailTimeline || []
|
||||
return isLifecycleExpanded.value ? rows : rows.slice(0, 3)
|
||||
})
|
||||
const filterBodyVisible = computed(() => isFilterPanelOpen.value || hasActiveFilters.value)
|
||||
const isHistoryLoading = ref(false)
|
||||
const historyRows = ref<AnalyticsFileRow[]>([])
|
||||
@@ -2051,6 +2073,13 @@ const getTimelineDotClass = (severity: AdminFileInsightSeverity) => {
|
||||
return classes[severity]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selectedFileDetail.value?.id,
|
||||
() => {
|
||||
isLifecycleExpanded.value = false
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => route.query.health,
|
||||
async () => {
|
||||
|
||||
Reference in New Issue
Block a user